You are on page 1of 205

UNIVERSIDAD DE MLAGA Dpto. Lenguajes y CC. Computacin E.T.S.I.

Informtica

Fundamentos de Programacin con el Lenguaje de Programacin C++


Vicente Benjumea y Manuel Roldn 30 de septiembre de 2013

Este obra est bajo una licencia Reconocimiento-NoComercial-CompartirIgual 3.0 Unported de Creative Commons: No se permite un uso comercial de la obra original ni de las posibles obras derivadas, la distribucin de las cuales se debe hacer con una licencia igual a la que regula la obra original. Para ver una copia de esta licencia, visite http://creativecommons.org/licenses/by-nc-sa/3.0/deed.es_ES o envie una carta a Creative Commons, 171 Second Street, Suite 300, San Francisco, California 94105, USA. Usted es libre de: Copiar, distribuir y comunicar pblicamente la obra. Hacer obras derivadas. Bajo las siguientes condiciones: Reconocimiento (Attribution) Debe reconocer los crditos de la obra de la manera especicada por el autor o el licenciador (pero no de una manera que sugiera que tiene su apoyo o apoyan el uso que hace de su obra). No comercial (Non commercial) No puede utilizar esta obra para nes comerciales. Compartir bajo la misma licencia (Share alike) Si altera o transforma esta obra, o genera una obra derivada, slo puede distribuir la obra generada bajo una licencia idntica a sta. Entendiendo que: Renuncia Alguna de estas condiciones puede no aplicarse si se obtiene el permiso del titular de los derechos de autor Dominio Pblico Cuando la obra o alguno de sus elementos se halle en el dominio pblico segn la ley vigente aplicable, esta situacin no quedar afectada por la licencia. Otros derechos Los derechos siguientes no quedan afectados por la licencia de ninguna manera: Los derechos derivados de usos legtimos u otras limitaciones reconocidas por ley no se ven afectados por lo anterior. Los derechos morales del autor Derechos que pueden ostentar otras personas sobre la propia obra o su uso, como por ejemplo derechos de imagen o de privacidad. Aviso Al reutilizar o distribuir la obra, tiene que dejar bien claro los trminos de la licencia de esta obra.

Dpto. Lenguajes y Ciencias de la Computacin

Universidad de Mlaga

ndice general
Prlogo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7

Programacin Bsica

9
11 15 15 16 17 17 19 20 21 21 22 22 22 25 25 26 27 29 31 31 31 32 37 41 42 45 45 46 48 48 50 50 51 51 52

1. Un Programa C++ 2. Tipos Simples 2.1. Declaracin Vs. Denicin . . . . . . . . . . . . . . 2.2. Tipos Simples Predenidos . . . . . . . . . . . . . 2.3. Tipos Simples Enumerados . . . . . . . . . . . . . 2.4. Constantes y Variables . . . . . . . . . . . . . . . . 2.5. Expresiones . . . . . . . . . . . . . . . . . . . . . . 2.6. Sentencias de Asignacin . . . . . . . . . . . . . . . 2.7. Visibilidad de los identicadores . . . . . . . . . . 2.8. Conversiones Automticas (Implcitas) de Tipos . . 2.9. Conversiones Explcitas de Tipos . . . . . . . . . . 2.10. Tabla ASCII . . . . . . . . . . . . . . . . . . . . . 2.11. Algunas consideraciones respecto a operaciones con 3. Entrada y Salida de Datos Bsica 3.1. El Buer de Entrada y el Buer 3.2. Salida de Datos . . . . . . . . . . . 3.3. Entrada de Datos . . . . . . . . . . 3.4. Control del Estado del Flujo . . . . 4. Estructuras de Control 4.1. Sentencia, Secuencia y Bloque . . 4.2. Declaraciones Globales y Locales 4.3. Sentencias de Seleccin . . . . . . 4.4. Sentencias de Iteracin. Bucles . 4.5. Programacin Estructurada . . . 4.6. Ejemplos . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . nmeros reales

. . . . . . . . . . .

. . . . . . . . . . .

. . . . . . . . . . .

. . . . . . . . . . .

. . . . . . . . . . .

. . . . . . . . . . .

. . . . . . . . . . .

. . . . . . . . . . .

. . . . . . . . . . .

de . . . . . .

Salida . . . . . . . . . . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

5. Subprogramas. Funciones y Procedimientos 5.1. Funciones y Procedimientos . . . . . . . . . . . . . . . . . . 5.2. Denicin de Subprogramas . . . . . . . . . . . . . . . . . . 5.3. Ejecucin de Subprogramas . . . . . . . . . . . . . . . . . . 5.4. Paso de Parmetros. Parmetros por Valor y por Referencia 5.5. Criterios de Modularizacin . . . . . . . . . . . . . . . . . . 5.6. Subprogramas en Lnea . . . . . . . . . . . . . . . . . . . 5.7. Declaracin de Subprogramas. Prototipos . . . . . . . . . . 5.8. Sobrecarga de Subprogramas . . . . . . . . . . . . . . . . . 5.9. Pre-Condiciones y Post-Condiciones . . . . . . . . . . . . . 3

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

NDICE GENERAL 5.10. Ejemplo. Nmeros primos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53 55 55 56 56 59 60 63 65 65 66 67 69 70 72 74 77 85 85 86 87 88 88 89 90 91 97

6. Tipos Compuestos 6.1. Paso de Parmetros de Tipos Compuestos . . . . . . . . . . . . . 6.2. Cadenas de Caracteres en C++: el Tipo string . . . . . . . . . 6.2.1. Entrada y Salida de Cadenas de Caracteres . . . . . . . . 6.2.2. Operadores predenidos . . . . . . . . . . . . . . . . . . . 6.2.3. Ejemplos . . . . . . . . . . . . . . . . . . . . . . . . . . . 6.3. Registros o Estructuras . . . . . . . . . . . . . . . . . . . . . . . 6.3.1. Operaciones con registros completos . . . . . . . . . . . . 6.3.2. Entrada/Salida de valores de tipo registro . . . . . . . . . 6.3.3. Ejemplo. Uso de registros . . . . . . . . . . . . . . . . . . 6.4. Agregados: el Tipo Array . . . . . . . . . . . . . . . . . . . . . . 6.4.1. Operadores predenidos . . . . . . . . . . . . . . . . . . . 6.4.2. Ejemplos . . . . . . . . . . . . . . . . . . . . . . . . . . . 6.4.3. Agregados Incompletos . . . . . . . . . . . . . . . . . . . 6.4.4. Agregados Multidimensionales . . . . . . . . . . . . . . . 6.5. Resolucin de Problemas Utilizando Tipos Compuestos. Agenda 7. Bsqueda y Ordenacin 7.1. Algoritmos de Bsqueda . . . . . . . . . . . . . . . . . . 7.1.1. Bsqueda Lineal (Secuencial) . . . . . . . . . . . 7.1.2. Bsqueda Binaria . . . . . . . . . . . . . . . . . 7.2. Algoritmos de ordenacin . . . . . . . . . . . . . . . . . 7.2.1. Ordenacin por Seleccin . . . . . . . . . . . . . 7.2.2. Ordenacin por Intercambio (Burbuja) . . . . . . 7.2.3. Ordenacin por Insercin . . . . . . . . . . . . . 7.3. Aplicacin de los Algoritmos de Bsqueda y Ordenacin 8. Algunas Bibliotecas tiles

. . . . . . . . . . . . . . .

. . . . . . . . . . . . . . .

. . . . . . . . . . . . . . .

. . . . . . . . . . . . . . .

. . . . . . . . . . . . . . .

. . . . . . . . . . . . . . .

. . . . . . . . . . . . . . .

. . . . . . . . . . . . . . .

. . . . . . . . . . . . . . .

. . . . . . . . . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

II

Programacin Intermedia
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

101
103 104 106 108 111

9. Almacenamiento en Memoria Secundaria: Ficheros 9.1. Flujos de Entrada y Salida Asociados a Ficheros . . 9.2. Entrada de Datos desde Ficheros de Texto . . . . . . 9.3. Salida de Datos a Ficheros de Texto . . . . . . . . . 9.4. Ejemplos . . . . . . . . . . . . . . . . . . . . . . . .

10.Mdulos y Bibliotecas 117 10.1. Interfaz e Implementacin del Mdulo . . . . . . . . . . . . . . . . . . . . . . . . . 117 10.2. Compilacin Separada y Enlazado . . . . . . . . . . . . . . . . . . . . . . . . . . . 119 10.3. Espacios de Nombres . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 120 11.Tipos Abstractos de Datos 11.1. Tipos Abstractos de Datos en C++: Clases . . . . . . 11.1.1. Denicin de Clases . . . . . . . . . . . . . . . 11.1.2. Utilizacin de Clases . . . . . . . . . . . . . . . 11.1.3. Implementacin de Clases . . . . . . . . . . . . 11.1.4. Ejemplo . . . . . . . . . . . . . . . . . . . . . . 11.2. Tipos Abstractos de Datos en C++: Ms sobre Clases 11.2.1. Ejemplo . . . . . . . . . . . . . . . . . . . . . .
Dpto. Lenguajes y Ciencias de la Computacin

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

129 130 130 132 134 135 140 146

Universidad de Mlaga

NDICE GENERAL

12.Introduccin a la Programacin Genrica. Plantillas 153 12.1. Subprogramas Genricos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 153 12.2. Tipos Abstractos de Datos Genricos . . . . . . . . . . . . . . . . . . . . . . . . . . 156 13.Memoria Dinmica. Punteros 13.1. Punteros . . . . . . . . . . . . . . . . . . . . . . . . 13.2. Gestin de Memoria Dinmica . . . . . . . . . . . 13.3. Operaciones con Variables de Tipo Puntero . . . . 13.4. Paso de Parmetros de Variables de Tipo Puntero 13.5. Listas Enlazadas Lineales . . . . . . . . . . . . . . 13.6. Abstraccin en la Gestin de Memoria Dinmica . 13.7. Tipo Abstracto de Datos Lista Enlazada Genrica 163 164 165 166 168 168 173 174 181 182 185 189 191 193 197 197 199 199 201 202 203 203

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

14.Introduccin a los Contenedores de la Biblioteca Estndar 14.1. Vector . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14.2. Deque . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14.3. Stack . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14.4. Queue . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14.5. Resolucin de Problemas Utilizando Contenedores . . . . . . 15.Introduccin a la Programacin Orientada a 15.1. Herencia . . . . . . . . . . . . . . . . . . . . . 15.2. Polimorsmo . . . . . . . . . . . . . . . . . . 15.3. Vinculacin Dinmica . . . . . . . . . . . . . 15.4. Clases Base Abstractas. Interfaces . . . . . . 15.5. Herencia Mltiple Virtual . . . . . . . . . . . 16.Bibliografa ndice Objetos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

(STL) . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

Dpto. Lenguajes y Ciencias de la Computacin

Universidad de Mlaga

NDICE GENERAL

Dpto. Lenguajes y Ciencias de la Computacin

Universidad de Mlaga

Prlogo
En este manual se describen las caractersticas bsicas del lenguaje C++. Est concebido desde el punto de vista docente, por lo que nuestra intencin no es hacer una descripcin completa del lenguaje, sino nicamente de aquellas caractersticas adecuadas como base para facilitar el aprendizaje en un primer curso de programacin. Se supone que el alumno compatibilizar el uso de este manual con las explicaciones del profesor, impartidas en el aula. El lenguaje de programacin C++ es un lenguaje muy exible y verstil. Debido a ello, si se utiliza sin rigor puede dar lugar a construcciones y estructuras de programacin complejas, difciles de comprender y propensas a errores. Por este motivo, restringiremos tanto las estructuras a utilizar como la forma de utilizarlas. Este manual ha sido elaborado en el Dpto. de Lenguajes y Ciencias de la Computacin de la Universidad de Mlaga La ltima versin de este documento puede ser descargada desde la siguiente pgina web:
http://www.lcc.uma.es/%7Evicente/docencia/index.html

o directamente desde la siguiente direccin:


http://www.lcc.uma.es/%7Evicente/docencia/cpp/programacion_cxx.pdf

NDICE GENERAL

Dpto. Lenguajes y Ciencias de la Computacin

Universidad de Mlaga

Parte I

Programacin Bsica

Captulo 1

Un Programa C++
En general, un programa C++ suele estar escrito en diferentes cheros. Durante el proceso de compilacin estos cheros sern combinados adecuadamente y traducidos a cdigo objeto, obteniendo el programa ejecutable. Nosotros comenzaremos tratando con programas sencillos, para los que bastar un nico chero cuya extensin ser una de las siguientes: .cpp, .cxx, .cc, etc. En el captulo 10 comenzaremos a estudiar cmo estructurar programas complejos en diferentes cheros. En este captulo nos centraremos en presentar los elementos imprescindibles y en mostrar cmo trabajar con el chero que contiene el programa para generar su correspondiente chero ejecutable. En posteriores captulos trataremos con detalle cada uno de los elementos que puede contener un programa. El chero suele comenzar con unas lneas para incluir las deniciones de los mdulos de biblioteca que utilice nuestro programa, e ir seguido de declaraciones y deniciones de tipos, de constantes y de subprogramas. El programa debe contener un subprograma especial (la funcin main ) que indica dnde comienza la ejecucin. Las instrucciones contenidas en dicha funcin main se ejecutarn una tras otra hasta llegar a su n. La funcin main devuelve un valor que indica si el programa ha sido ejecutado correctamente o, por el contrario, ha ocurrido un error. En caso de no aparecer explcitamente una sentencia return, por defecto, se devolver un valor que indica terminacin normal (0). A continuacin, mostramos un programa que convierte una cantidad determinada de euros a su valor en pesetas y describimos cmo hay que proceder para obtener el programa ejecutable correspondiente. Ms adelante iremos introduciendo cada uno de los elementos que lo forman.
//- fichero: euros.cpp -------------------------------------------#include <iostream> using namespace std ; const double EUR_PTS = 166.386 ; int main() { cout << "Introduce la cantidad (en euros): " ; double euros ; cin >> euros ; double pesetas = euros * EUR_PTS ; cout << euros << " Euros equivalen a " << pesetas << " Pts" << endl ; // return 0 ; } //- fin: euros.cpp ------------------------------------------------

Para obtener el programa ejecutable necesitamos dos herramientas bsicas: un editor con el que crear un archivo con el texto del mismo (el cdigo fuente) y un compilador para traducir el cdigo fuente a cdigo ejecutable. Para realizar estos pasos podemos seguir dos enfoques diferentes: usar directamente la lnea de comandos del sistema operativo o usar un entorno integrado de desarrollo (IDE). 11

12

CAPTULO 1. UN PROGRAMA C++

Si optamos por seguir los pasos desde la lnea de comandos (en un entorno linux) podemos usar cualquier editor de texto (emacs, vi, gedit, kate, etc.) para crear un chero (por ejemplo, euros.cpp) y un compilador como GNU GCC. En este caso compilaremos de la siguiente forma:
g++ -ansi -Wall -Werror -o euros euros.cpp

Durante el proceso de compilacin pueden aparecer errores, que habr que solucionar. Cada vez que modiquemos el chero para corregir errores deberemos volver a compilar, hasta que no quede ningn error. En ese momento ya tenemos un chero ejecutable (denominado euros) y podemos proceder a su ejecucin como se muestra a continuacin, donde el texto enmarcado corresponde a una entrada de datos por parte del usuario:
Introduce la cantidad (en euros): 3.5 ENTER 3.5 Euros equivalen a 582.351 Pts

En un entorno integrado de desarrollo (por ejemplo, Code::Blocks) disponemos de un conjunto de ventanas y de botones asociados a las diferentes herramientas mencionadas. Seguiremos los mismos pasos pero, en lugar de hacerlo desde la lnea de comandos, trabajaremos en ventanas propias del entorno y utilizando los botones asociados a las diferentes herramientas indicadas anteriormente. A continuacin, utilizaremos nuestro programa euros.cpp para introducir los elementos bsicos de un programa. Posteriormente, todos estos elementos sern tratados con mayor amplitud en el captulo que corresponda. Bibliotecas El lenguaje C++ consta de un reducido nmero de instrucciones, pero ofrece un amplio repertorio de bibliotecas con herramientas que pueden ser importadas por los programas cuando son necesarias. Por este motivo, un programa suele comenzar por tantas lneas #include como bibliotecas se necesiten. Como se puede observar, en nuestro ejemplo se incluye la biblioteca iostream, necesaria cuando se van a efectuar operaciones de entrada (lectura de datos) o salida (escritura de datos). Para utilizar la biblioteca iostream es necesario utilizar el espacio de nombres std, ste es un concepto avanzado que, por ahora, est fuera de nuestro mbito de estudio (se estudiar en el captulo 10). Por ahora nos basta con recordar que nuestros programas deben contener la siguiente directiva:
using namespace std ;

Constantes simblicas y constantes literales En nuestro ejemplo, sabemos que cada euro equivale a 166.386 pesetas y usamos dicho valor para efectuar operaciones con las que convertir de unas unidades a otras. Podemos emplear dicho valor en su sentido literal (tal y como est escrito), sin embargo, hemos preferido denir un nombre para referirnos al mismo. De esta forma, durante el programa en lugar de referirnos al factor de conversion mediante su valor literal (166.386), podremos usar la nueva constante simblica EUR_PTS. Ello resulta ms claro y permitir que el programa sea ms legible y fcil de modicar en el futuro. Antes de utilizar una constante simblica es necesario informar al compilador, indicando de qu tipo es, su nombre (su identicador ) y el valor que tiene asociado. Si no lo hiciramos, el compilador no sera capaz de reconocer qu es EUR_PTS y nos mostrara un mensaje de error. En nuestro ejemplo utilizamos lo siguiente:
const double EUR_PTS = 166.386 ;

Identicadores Para cada elemento que introduzcamos en nuestro programa debemos denir un nombre (identicador) con el que hacer referencia al mismo y disponer de algn mecanismo para informar al compilador de dicho nombre y de sus caractersticas. Hemos visto cmo hacerlo para
Dpto. Lenguajes y Ciencias de la Computacin Universidad de Mlaga

13 constantes simblicas, pero de igual forma habr que proceder con otro tipo de elementos, como variables, tipos, subprogramas, etc, que iremos tratando en posteriores captulos. En C++ se considera que las letras minsculas son caracteres diferentes de las letras maysculas. Como consecuencia, si en nuestro ejemplo hubiramos usado el identicador eur_pts el compilador considerara que no se trata de EUR_PTS, informando de un mensaje de error al no poder reconocerlo. Pruebe, por ejemplo, a sustituir en la siguiente lnea el identicador EUR_PTS por eur_pts y vuelva a compilar el programa. Podr comprobar que el compilador informa del error con un mensaje adecuado al mismo.
double pesetas = euros * EUR_PTS ;

Un identicador debe estar formado por una secuencia de letras y dgitos en la que el primer carcter debe ser una letra. El carcter _ se considera como una letra, sin embargo, los nombres que comienzan con dicho carcter se reservan para situaciones especiales, por lo que no deberan utilizarse en programas. Aunque el lenguaje no impone ninguna restriccin adicional, en este manual seguiremos unos criterios de estilo a la hora decidir qu identicador utilizamos para cada elemento. Ello contribuye a mejorar la legibilidad del programa. Por ejemplo, los identicadores utilizados como nombres de constantes simblicas estarn formados por letras maysculas y, en caso de querer que tengan ms de una palabra, usaremos _ como carcter de separacin (EUR_PTS). En posteriores captulos iremos proporcionando otros criterios de estilo para ayudar a construir identicadores para variables, tipos, funciones, etc. Palabras reservadas Algunas palabras tienen un signicado especial en el lenguaje y no pueden ser utilizadas con otro sentido. Por este motivo no pueden ser utilizados para designar elementos denidos por el programador. Son palabras reservadas, como por ejemplo: using, namespace, const, double, int, char, bool, void, for, while, do, if, switch, case, default, return, typedef, enum, struct, etc. Delimitadores Son smbolos que indican comienzo o n de una entidad (( ) { } ; , < >). Por ejemplo, en nuestro programa euros.cpp usamos { y } para delimitar el comienzo y el nal de la funcin main, y el smbolo ; para delimitar el nal de una sentencia. Operadores Son smbolos con signicado propio segn el contexto en el que se utilicen. Ejemplo: = << >> * / % + - < > <= >= == != ++ -- . , etc. Comentarios y formato del programa El compilador necesita reconocer los diferentes elementos que forman el programa. Para ello, utiliza delimitadores y caracteres que permiten separar unos elementos con otros. Adems, el programador puede aadir caracteres como espacios en blanco, lneas en blanco o tabuladores para mejorar la legibilidad del programa. El compilador los ignora, salvo que les sirva como separadores de unos elementos y otros. Adems, el programador puede estar interesado en mejorar la legibilidad del programa incluyendo comentarios dirigidos a otros programadores, no al compilador. Para ello, debe marcar dicho texto de alguna forma que permita ser identicado por el compilador. En C++ esto se puede hacer de dos formas diferentes: Comentarios hasta n de lnea: los smbolos // marcan el comienzo del comentario, que se extiende hasta el nal de la lnea.
//- fichero: euros.cpp -------------------------------------------Dpto. Lenguajes y Ciencias de la Computacin Universidad de Mlaga

14

CAPTULO 1. UN PROGRAMA C++ Comentarios enmarcados: los smbolos /* marcan el comienzo del comentario, que se extiende hasta los smbolos del n del comentario */. Por ejemplo, podramos haber incluido al principio del chero euros.cpp algunas lneas de comentario informando de su autor y fecha de elaboracin.
/* * Autor: Juan Gil * Fecha: 12/09/2012 */

Variables Los datos se almacenan en memoria en variables de un cierto tipo. El programador debe decidir qu variables va a utilizar, pensar en un identicador para referirse a ellas y comunicarlo al compilador. En nuestro programa usamos una variable llamada euros para almacenar la cantidad de euros a convertir a pesetas. Usamos la siguiente denicin para informar al compilador que euros es una variable que contiene un nmero real:
double euros ;

Entrada/Salida de datos En general, un programa necesitar tomar datos de entrada y mostrar resultados en algn dispositivo de salida. En C++ disponemos de ujos de entrada (cin) y de salida (cout), que nos permiten efectuar entrada y salida de datos, respectivamente. El operador >> se usa para tomar un valor de la entrada y el operador << se usa para sacar un valor por la salida. En nuestro ejemplo, la siguiente sentencia muestra por pantalla un mensaje en el que se solicita que el usuario introduzca un nmero que representa los euros que se desean convertir a pesetas:
cout << "Introduce la cantidad (en euros): " ;

El texto entre comillas es una constante literal, que aparecer tal y como est escrito en el dispositivo de salida. Si en lugar de una constante literal hubiramos usado un identicador con el nombre de una constante o de una variable, entonces el valor resultante en la salida sera aqul asociado a dicho identicador. En la siguiente lnea sacamos por la salida una combinacin de valores almacenados en variables y de constantes literales (entre comillas):
cout << euros << " Euros equivalen a " << pesetas << " Pts" << endl ;

En nuestro ejemplo necesitamos que el usuario introduzca como entrada un nmero que representa los euros a convertir en pesetas. El nmero introducido debe ser almacenado en alguna variable para poder ser posteriormente manipulado en el programa. Por ese motivo, informamos de la variable que vamos a utilizar (euros). En la siguiente sentencia utilizamos el operador >> para tomar un nmero del ujo de entrada (cin) y almacenarlo en dicha variable.
cin >> euros ;

Asignacin La conversin de euros a pesetas se realiza mediante un clculo simple, en el que se utiliza el factor de conversin. En nuestro programa queremos realizar el clculo y almacenar el resultado de manera que pueda ser utilizado posteriormente. Para ello utilizamos una instrucin de asignacin, en la que el operador = separa el valor a almacenar, resultado de evaluar una expresin aritmtica (a la derecha), de la variable en la que lo queremos almacenar (asignar ).
double pesetas = euros * EUR_PTS ;

Dpto. Lenguajes y Ciencias de la Computacin

Universidad de Mlaga

Captulo 2

Tipos Simples
Un programa trabaja con datos con unas determinadas caractersticas. No es lo mismo procesar nmeros naturales, nmeros reales o nombres (cadenas de caracteres). En cada caso tratamos con datos de tipo diferente. C++ dispone de un nmero reducido de tipos predenidos y es importante elegir en cada caso aquel que mejor se adapta a los datos que deseamos manipular. Por ejemplo, en nuestro programa euros.cpp hicimos uso del tipo double porque nuestro objetivo era manipular datos numricos, posiblemente con decimales. Tambin es posible denir nuevos tipos, con caractersticas no predenidas sino pensadas especcamente para un determinado programa. La eleccin de un determinado tipo u otro para una determinada entidad del programa proporciona informacin acerca de: El rango de posibles valores que puede tomar. El conjunto de operaciones y manipulaciones aplicables. El espacio de almacenamiento necesario para almacenar dichos valores. La interpretacin del valor almacenado. Por ejemplo, al especicar que un dato es de tipo double estamos indicando que se trata de un nmero real, representado internamente en punto otante con doble precisin (normalmente usando 8 bytes) y que se puede manipular con los operadores predenidos +, -, * y /. Los tipos se pueden clasicar en simples y compuestos. Los tipos simples se caracterizan porque sus valores son indivisibles, es decir, no se dispone de operadores para acceder a parte de ellos. Por ejemplo, si 126.48 es un valor de tipo double, podemos usar el operador + para sumarlo con otro, pero no disponemos de operadores predenidos para acceder a sus dgitos individualmente (aunque esto se pueda realizar indirectamente mediante operaciones de bits o aritmticas). Por el contrario, los tipos compuestos (ver el captulo 6) estn formados como un agregado o composicin de otros tipos, ya sean simples o compuestos.

2.1.

Declaracin Vs. Denicin

Sabemos que el compilador necesita ser informado de cada identicador que sea utilizado en el programa. Para ello se usa una declaracin, con la que se presenta dicho identicador, haciendo referencia a una determinada entidad que debe ser denida en otro lugar del programa. Con una denicin se establecen las caractersticas de una determinada entidad, asociada a un determinado identicador. Toda denicin es a su vez tambin una declaracin. Es obligatorio que por cada entidad slo exista una nica denicin, aunque pueden existir varias declaraciones. As mismo, tambin es obligatorio la declaracin de las entidades que se manipulen en el programa, especicando su tipo, identicador, valores, etc. antes de que sean utilizados. 15

16

CAPTULO 2. TIPOS SIMPLES

Por ejemplo, el programa euros.cpp contiene la denicin de la constante EUR_PTS y de las variables euros y pesetas. Dichas deniciones son, a su vez, declaraciones con las que se presentan dichos identicadores al compilador.

2.2.

Tipos Simples Predenidos

El lenguaje de programacin C++ proporciona los siguientes tipos simples predenidos :


int float double bool char

El tipo int se utiliza para trabajar con nmeros enteros. Su representacin suele coincidir con la denida por el tamao de palabra del procesador sobre el que va a ser ejecutado, hoy da suele ser de 4 bytes (32 bits), aunque en determinados ordenadores puede ser de 8 bytes (64 bits). Puede ser modicado para representar un rango de valores menor, mediante el modicador short (normalmente 2 bytes [16 bits]) o para representar un rango de valores mayor, mediante el modicador long (normalmente 4 bytes [32 bits] u 8 bytes [64 bits]) y long long (normalmente 8 bytes [64 bits]). Tambin puede ser modicado para representar solamente nmeros naturales (enteros positivos) utilizando el modicador unsigned. Tanto el tipo float como el double se utilizan para representar nmeros reales en formato de punto otante. Se diferencian en el rango de valores que se utiliza para su representacin interna. El tipo double (doble precisin) se suele representar utilizando 8 bytes ([64 bits]), mientras que el tipo float (simple precisin) se suele representar utilizando 4 bytes [32 bits]). El tipo double tambin puede ser modicado con long para representar cudruple precisin (normalmente 12 bytes [96 bits]). El tipo bool se utiliza para representar valores lgicos (o booleanos), es decir, los valores Verdadero o Falso o las constantes lgicas true y false. Suele almacenarse en el tamao de palabra ms pequeo posible direccionable (normalmente 1 byte). El tipo char se utiliza para representar caracteres, es decir, smbolos alfanumricos (dgitos y letras maysculas y minsculas), de puntuacin, espacios, control, etc. Normalmente utiliza un espacio de almacenamiento de 1 byte (8 bits) y puede representar 256 valores diferentes (vase 2.10). Todos los tipos simples tienen la propiedad de ser indivisibles y adems, mantener una relacin de orden entre sus elementos (se les pueden aplicar los operadores relacionales, vase 2.5). Se les conoce tambin como tipos escalares . Todos ellos, salvo los de punto otante (float y double), tienen tambin la propiedad de que cada posible valor tiene un nico antecesor y un nico sucesor. A stos se les conoce como tipos ordinales (en terminologa C++, tambin se les conoce como tipos integrales, o enteros). La siguiente tabla muestra las caractersticas fundamentales de los tipos simples predenidos.
Tipo bool char short int long long long unsigned char unsigned short unsigned unsigned long unsigned long long float double long double Bytes 1 1 2 4 4 8 1 2 4 4 8 4 8 12 Bits 8 8 16 32 32 64 8 16 32 32 64 32 64 96 Min.Valor false -128 -32768 -2147483648 -2147483648 -9223372036854775808 0 0 0 0 0 1.17549435e-38 2.2250738585072014e-308 3.36210314311209350626e-4932 Max.Valor true 127 32767 2147483647 2147483647 9223372036854775807 255 65535 4294967295 4294967295 18446744073709551615 3.40282347e+38 1.7976931348623157e+308 1.18973149535723176502e+4932

Dpto. Lenguajes y Ciencias de la Computacin

Universidad de Mlaga

2.3. TIPOS SIMPLES ENUMERADOS

17

2.3.

Tipos Simples Enumerados

En ocasiones el programador puede estar interesado en trabajar con tipos simples cuyos valores no coinciden con las caractersticas de ninguno de los tipos simples predenidos. Entonces puede denir un nuevo tipo enumerado , cuyos valores sern aquellos que explcitamente se enumeren. De esta forma se consigue disponer tipos que expresan mejor las caractersticas de las entidades manipuladas por el programa, por lo que el programa ser ms legible y fcil de entender. Por ejemplo, si en un programa quisiramos tratar con colores, con lo visto hasta ahora, para hacer referencia a un determinado color habra que seleccionar un tipo predenido (por ejemplo, char) y suponer que cada color ser representado por un cierto carcter (por ejemplo, A para el color azul). En su lugar, podemos denir un nuevo tipo Color que contenga AZUL como uno de sus valores predenidos. Para ello, haramos lo siguiente:
enum Color { ROJO, AZUL, AMARILLO } ;

En realidad, internamente cada valor de la enumeracin se corresponde con un valor natural, siendo el primero el cero e incrementndose de uno en uno. En nuestro ejemplo ROJO se corresponde internamente con 0, AZUL con 1, etc.. Los tipos enumerados, al ser tipos denidos por el programador, no tiene entrada ni salida predenida por el lenguaje, sino que deber ser el programador el que especique (programe) como se realizar la entrada y salida de datos en caso de ser necesaria.

2.4.

Constantes y Variables

Podemos dividir las entidades que nuestro programa manipula en dos clases fundamentales: aquellas cuyo valor no vara durante la ejecucin del programa (constantes ) y aquellas otros cuyo valor puede ir cambiando durante la ejecucin del programa (variables ).

Constantes
A su vez, las constantes pueden aparecer como constantes literales , aquellas cuyo valor aparece directamente en el programa, y como constantes simblicas , aquellas cuyo valor se asocia a un identicador, a travs del cual se representa. Ejemplos de constantes literales: Constantes lgicas (bool):
false, true

Constantes carcter (char), el smbolo constante aparece entre comillas simples:


a, A, 0, , b, B, 1, ., ..., ..., ..., ,, z, Z, 9, :, ;, ...

As mismo, ciertos caracteres constantes tienen un signicado especial (caracteres de escape):


\n: n de lnea (newline) \r: retorno de carro (carriage-return) \b: retroceso (backspace) \t: tabulador horizontal \v: tabulador vertical
Dpto. Lenguajes y Ciencias de la Computacin Universidad de Mlaga

18
\f: avance de pgina (form-feed) \a: sonido (audible-bell) \0: n de cadena

CAPTULO 2. TIPOS SIMPLES

\137, \x5F: carcter correspondiente al valor especicado en notacin octal y hexadecimal respectivamente

Constantes cadenas de caracteres literales, la secuencia de caracteres aparece entre comillas dobles (puede contener caracteres de escape):
"Hola Pepe" "Hola\nJuan\n" "Hola " "Mara"

Constantes enteras, pueden ser expresadas en decimal (base 10), hexadecimal (base 16) y octal (base 8). El sujo L se utiliza para especicar long, el sujo LL se utiliza para especicar long long, el sujo U se utiliza para especicar unsigned, el sujo UL especica unsigned long, y el sujo ULL especica unsigned long long:
123, -1520, 2345U, 30000L, 50000UL, 0x10B3FC23 (hexadecimal), 0751 (octal)

Constantes reales, nmeros en punto otante. El sujo F especica float, y el sujo L especica long double:
3.1415, -1e12, 5.3456e-5, 2.54e-1F, 3.25e200L

dnde la notacin -1e12 representa el nmero real 1 101 2 y 5.3456e-5 representa el nmero real 5 105 . Constantes Simblicas Para declarar una constante simblica se usa la palabra reservada const, seguida por su tipo, el nombre simblico (o identicador) con el que nos referiremos a ella y el valor asociado tras el smbolo (=). Usualmente se denen al principio del programa, despus de la inclusin de las cabeceras de las bibliotecas. Ejemplos de constantes simblicas:
const const const const const const const const const const const const const const const const bool OK = true ; char SONIDO = \a ; short ELEMENTO = 1000 ; unsigned SEGMIN = 60 ; unsigned MINHOR = 60 ; unsigned SEGHOR = SEGMIN * MINHOR ; long ULTIMO = 100000L ; long long TAMANO = 1000000LL ; unsigned short VALOR = 100U ; unsigned FILAS = 200U ; unsigned long COLUMNAS = 200UL ; unsigned long long NELMS = 2000ULL ; float N_E = 2.7182F ; double LOG10E = log(N_E) ; long double N_PI = 3.141592L ; Color COLOR_DEFECTO = ROJO ;

Variables
Las variables se denen mediante sentencias en las que se especica el tipo y una lista de identicadores separados por comas. Todos los identicadores de la lista sern variables de dicho tipo. En dicha sentencia es posible asignar un valor inicial a una variable. Para ello, usamos el smbolo = seguido de una expresin con la que se calcula el valor inicial de la variable. En caso de que no se asigne ningn valor inicial, la variable tendr un valor inespecicado.
Dpto. Lenguajes y Ciencias de la Computacin Universidad de Mlaga

2.5. EXPRESIONES

19

El valor de una variable podr cambiar utilizando sentencia de asignacin (vase 2.6) o mediante una sentencia de entrada de datos (vase 3.3).
char letra ; // valor inicial inespecificado int edad = 10, contador = 0 ; double suma, total = 5.0 ;

2.5.

Expresiones

Un programa se basa en la realizacin de una serie de clculos con los que se producen los resultados deseados. Dichos resultados se almacenan en variables y a su vez son utilizados para nuevos clculos, hasta obtener el resultado nal. Los clculos se producen mediante la evaluacin de expresiones en las que se mezclan operadores con operandos (constantes literales, constantes simblicas o variables). En caso de que en una misma expresin se utilice ms de un operador habr que conocer la precedencia de cada operador (aplicando el de mayor precedencia), y en caso de que haya varios operadores de igual precedencia, habr que conocer su asociatividad (si se aplican de izquierda a derecha o de derecha a izquierda). A continuaccin mostramos los operadores disponibles (ordenados de mayor a menor precedencia), aunque por ahora nicamente utilizaremos los ms simples e intuitivos:
Operador [] -> . ! ~ - * * / % + << >> < <= > >= == != & ^ | && || ?: Tipo de Operador Binarios Unarios Binarios Binarios Binarios Binarios Binarios Binario Binario Binario Binario Binario Ternario Asociatividad Izq. a Dch. Dch. a Izq. Izq. a Dch. Izq. a Dch. Izq. a Dch. Izq. a Dch. Izq. a Dch. Izq. a Dch. Izq. a Dch. Izq. a Dch. Izq. a Dch. Izq. a Dch. Dch. a Izq.

Signicado de los operadores: Aritmticos. El resultado es del mismo tipo que los operandos (vase 2.8):
- valor valor * valor valor / valor valor % valor valor + valor valor - valor Menos unario Producto (multiplicacin) Divisin (entera o real segn el tipo de operandos) Mdulo (resto de la divisin) (slo tipos enteros) Suma Resta

Relacionales/Comparaciones. El resultado es de tipo bool


valor < valor valor <= valor valor > valor valor >= valor valor == valor valor != valor Comparacin Comparacin Comparacin Comparacin Comparacin Comparacin menor menor o igual mayor mayor o igual igualdad desigualdad

Operadores de bits, slo aplicable a operandos de tipos enteros. El resultado es del mismo tipo que los operandos:
Dpto. Lenguajes y Ciencias de la Computacin Universidad de Mlaga

20
~ valor valor << despl valor >> despl valor & valor valor ^ valor valor | valor Negacin de bits (complemento) Desplazamiento de bits a la izq. Desplazamiento de bits a la dch. AND de bits XOR de bits OR de bits

CAPTULO 2. TIPOS SIMPLES

Lgicos, slo aplicable operandos de tipo booleano. Tanto el operador && como el operador || se evalan en cortocircuito. El resultado es de tipo bool:
! valor valor1 && valor2 valor1 || valor2 Negacin lgica AND lgico OR lgico x F T ! x T F x F F T T
(Si valor es true entonces false, en otro caso true) (Si valor1 es false entonces false, en otro caso valor2) (Si valor1 es true entonces true, en otro caso valor2)

y F T F T

x && y F F F T

x || y F T T T

Condicional. El resultado es del mismo tipo que los operandos:


cond ? valor1 : valor2 Si cond es true entonces valor1, en otro caso valor2

2.6.

Sentencias de Asignacin

Una sentencia de asignacin permite almacenar en una variable el resultado de evaluar una expresin. La variable a la que se asigna un valor debe aparecer a la izquierda de la sentencia, separada de la expresin por el operador de asignacin (=). La ejecucin de una sentencia de asignacin consta de dos pasos: en primer lugar se evala la expresin que aparece a la derecha (considerando las reglas de precedencia y asociatividad de los operadores) y posteriormente se guarda el valor resultante en la variable que aparece a la izquierda. El valor que hubiera anteriormente almacenado en dicha variable se perder, siendo sustituido por el nuevo valor asignado.
<variable> = <expresin> ;

Por ejemplo:
const int MAXIMO = 15 ; int main() { int cnt ; // valor inicial inespecificado cnt = 30 * MAXIMO + 1 ; // asigna a cnt el valor 451 cnt = cnt + 10 ; // cnt pasa ahora a contener el valor 461 }

En C++ se dispone de operadores para escribir de forma abreviada determinadas expresiones tpicas de asignacin. Por ejemplo, es frecuente que surja la necesidad de sumar uno al contenido de cierta variable v. Para ello podra ejecutar la siguiente sentencia:
v = v + 1 ;

aunque tambin es posible obtener el mismo resultado con ++v; o v++;. Ntese que aunque en realidad ambas sentencias dan el mismo resultado, en realidad no son equivalentes. En este curso usaremos las sentencias de asignacin de forma que siempre sern equivalentes. Hay otros operadores abreviados de asignacin, que mostramos en la siguiente tabla:
Dpto. Lenguajes y Ciencias de la Computacin Universidad de Mlaga

2.7. VISIBILIDAD DE LOS IDENTIFICADORES Sentencia


++variable; --variable; variable++; variable--; variable variable variable variable variable variable variable variable variable variable += expresion; -= expresion; *= expresion; /= expresion; %= expresion; &= expresion; ^= expresion; |= expresion; <<= expresion; >>= expresion;

21

Equivalencia
variable variable variable variable variable variable variable variable variable variable variable variable variable variable = = = = = = = = = = = = = = variable variable variable variable variable variable variable variable variable variable variable variable variable variable + + 1; 1; 1; 1;

+ (expresion); - (expresion); * (expresion); / (expresion); % (expresion); & (expresion); ^ (expresion); | (expresion); << (expresion); >> (expresion);

2.7.

Visibilidad de los identicadores

El compilador necesita saber a qu entidad nos estamos reriendo cuando utilizamos un determinado identicador en un programa. Para ello, es necesario que presentemos (declaremos) cada nuevo identicador, as como conocer las reglas que se usan para determinar en qu parte del programa es visible (utilizable). Ello requiere introducir el concepto de bloque. Un bloque es una agrupacin de instrucciones delimitadas por { y }. Si un identicador es declarado dentro de un bloque (como ocurre con las variables), su mbito de visibilidad va desde el punto de la declaracin hasta el nal de dicho bloque. Sin embargo, si es declarado fuera de cualquier bloque (como ocurre con las constantes simblicas), su mbito de visibilidad abarca desde el punto de su declaracin hasta el nal de la unidad de compilacin (por ahora supondremos que la unidad de compilacin coincide con el chero en el que est escrito el programa). Como consecuencia de la aplicacin de esta regla, debemos efectuar la declaracin de la variable antes de su uso en un bloque. Por ejemplo, el siguiente trozo de programa ocasionara un error por parte del compilador
{ cnt = 0 ; int cnt ; }

2.8.

Conversiones Automticas (Implcitas) de Tipos

Es posible que nos interese realizar operaciones en las que se mezclen datos de tipos diferentes. El lenguaje de programacin C++ realiza conversiones de tipo automticas ( castings ), de tal forma que el resultado de la operacin sea del tipo ms amplio de los implicados en ella. Siempre que sea posible, los valores se convierten de tal forma que no se pierda informacin. Aunque C++ utiliza reglas mucho ms detalladas para efectuar conversiones implcitas de tipos, bsicamente se utilizan dos: Promocin: en cualquier operacin en la que aparezcan dos tipos diferentes se eleva el rango del que lo tiene menor para igualarlo al del mayor. El rango de los tipos de mayor a menor es el siguiente:
double, float, long, int, short, char

Almacenamiento: En una sentencia de asignacin, el resultado nal de los clculos se reconvierte al tipo de la variable al que est siendo asignado.
Dpto. Lenguajes y Ciencias de la Computacin Universidad de Mlaga

22

CAPTULO 2. TIPOS SIMPLES

2.9.

Conversiones Explcitas de Tipos

Tambin es posible realizar conversiones de tipo explcitas . Para ello, se escribe el tipo al que queremos convertir y entre parntesis la expresin cuyo valor queremos convertir. Por ejemplo:
char x = char(65) ; int x = int(a) ; int x = int(ROJO) ; int x = int(AMARILLO) ; int x = int(3.7) ; double x = double(2) ; Color x = Color(1) ; Color x = Color(c+1) ; produce el carcter A a partir de su cdigo ASCII (65) convierte el carcter a a su valor entero (97) produce el entero 0 produce el entero 2 produce el entero 3 produce el real (doble precisin) 2.0 produce el Color AZUL si c es de tipo Color, produce el siguiente valor de la enumeracin

El tipo enumerado se convierte automticamente a entero, sin embargo la conversin inversa no se realiza de forma automtica. En tal caso sera necesario efectuar una conversin explcita de tipo. Por ejemplo, para incrementar el valor de una variable del tipo enumerado Color, podemos sumar uno a la variable c (aqu se hace una conversin implcita), y efectuar una conversin explcita del nuevo valor obtenido (que ahora es de tipo entero) al tipo Color.
enum Color { ROJO, AZUL, AMARILLO } ; int main() { Color c = ROJO ; c = Color(c + 1) ; // ahora c tiene el valor AZUL }

2.10.

Tabla ASCII

La tabla ASCII es comnmente utilizada como base para la representacin de los caracteres, donde los nmeros del 0 al 31 se utilizan para representar caracteres de control, y los nmeros del 128 al 255 se utilizan para representar caracteres extendidos.
Rep 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 Simb \0 SOH STX ETX EOT ENQ ACK \a \b \t \n \v \f \r SO SI Rep 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 Simb DLE DC1 DC2 DC3 DC4 NAK SYN ETB CAN EM SUB ESC FS GS RS US Rep 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 Simb SP ! " # $ % & ( ) * + , . / Rep 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 Simb 0 1 2 3 4 5 6 7 8 9 : ; < = > ? Rep 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 Simb @ A B C D E F G H I J K L M N O Rep 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 Simb P Q R S T U V W X Y Z [ \ ] ^ _ Rep 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 Simb a b c d e f g h i j k l m n o Rep 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 Simb p q r s t u v w x y z { | } ~ DEL

2.11.

Algunas consideraciones respecto a operaciones con nmeros reales

La representacin interna de los nmeros reales utiliza un nmero nito de dgitos decimales. Ello hace que tanto el rango de valores que se pueden almacenar como su precisin estn limitados.
Dpto. Lenguajes y Ciencias de la Computacin Universidad de Mlaga

2.11. ALGUNAS CONSIDERACIONES RESPECTO A OPERACIONES CON NMEROS REALES23 Adems, la representacin de nmeros reales en base 2 hace que la representacin de determinados nmeros en base 10 sea inexacta. Como consecuencia, la realizacin de operaciones aritmticas en punto otante puede dar lugar a prdidas de precisin que ocasionen resultados inesperados. Distintas operaciones, que matemticamente son equivalentes, pueden ser computacionalmente diferentes. Para evitar que ello ocasione errores difciles de detectar, evitaremos la comparacin directa (de igualdad o desigualdad) de nmeros reales. Si nos interesa efectuar la comparacin tendremos en cuenta el error admitido en la misma. En este momento aun no hemos presentado todos los elementos necesarios para poder entender cmo y por qu hacer esto, por lo que retomaremos este punto ms adelante.
#include <iostream> using namespace std ; int main() { bool ok = (3.0 * (0.1 / 3.0)) == ((3.0 * 0.1) / 3.0) ; cout << "Resultado de (3.0 * (0.1 / 3.0)) == ((3.0 * 0.1) / 3.0): " << boolalpha << ok << endl ; }

produce un resultado distinto a lo que cabra esperar:


Resultado de (3.0 * (0.1 / 3.0)) == ((3.0 * 0.1) / 3.0): false

Dpto. Lenguajes y Ciencias de la Computacin

Universidad de Mlaga

24

CAPTULO 2. TIPOS SIMPLES

Dpto. Lenguajes y Ciencias de la Computacin

Universidad de Mlaga

Captulo 3

Entrada y Salida de Datos Bsica


La entrada y salida de datos permite a un programa recibir informacin desde el exterior (usualmente el teclado), la cual ser transformada mediante un determinado procesamiento, y posteriormente permitir mostrar al exterior (usualmente la pantalla del monitor) el resultado de la computacin. Para poder realizar entrada y salida de datos bsica es necesario incluir la biblioteca iostream, que contiene las declaraciones de tipos y operaciones que la realizan. Todas las deniciones y declaraciones de la biblioteca estndar se encuentran bajo el espacio de nombres std (ver captulo 10), por lo que para utilizarlos adecuadamente habr que utilizar la directiva using al comienzo del programa.
#include <iostream> // inclusin de la biblioteca de entrada/salida using namespace std ; // utilizacin del espacio de nombres de la biblioteca const double EUR_PTS = 166.386 ; int main() { cout << "Introduce la cantidad (en euros): " ; double euros ; cin >> euros ; double pesetas = euros * EUR_PTS ; cout << euros << " Euros equivalen a " << pesetas << " Pts" << endl ; }

3.1.

El Buer de Entrada y el Buer de Salida

Ningn dato de entrada o de salida en un programa C++ se obtiene o enva directamente del/al hardware, sino a travs de unas zonas de memoria intermedia (buers de entrada y salida, respectivamente), controlados por el Sistema Operativo e independientes de la ejecucin de nuestro programa. As, cuando se pulsa alguna tecla, el Sistema Operativo almacena en secuencia las teclas pulsadas en una zona de memoria intermedia: el buer de entrada. Cuando un programa realiza una operacin de entrada de datos (por ejemplo, cin >> valor), accede al buer asociado al ujo de entrada cin y obtiene la secuencia de caracteres all almacenados (si los hubiera) o, en caso de estar vaco, esperar hasta que los haya (hasta que el usuario pulse una serie de teclas seguidas por la tecla ENTER ). Una vez obtenida la secuencia de caracteres entrada por el usuario, se convertir a un valor del tipo especicado por la operacin de entrada, y dicho valor se asignar a la variable especicada. De igual forma, cuando se va a mostrar alguna informacin de salida (por ejemplo, cout << val), dichos datos no van directamente a la pantalla, sino que se convierten a un formato adecuado para ser impresos (una secuencia de caracteres) y se almacenan en una zona de memoria intermedia denominada buerde salida, asociado al ujo de salida cout, desde donde el Sistema Operativo tomar la informacin para ser mostrada por pantalla. 25

26

CAPTULO 3. ENTRADA Y SALIDA DE DATOS BSICA

3.2.

Salida de Datos

La salida de datos permite mostrar informacin al exterior, y se realiza a travs de los ujos de salida. El ujo de salida asociado a la salida estndar (usualmente la pantalla o terminal de la consola) se denomina cout. La salida de datos a pantalla se realiza utilizando el operador << sobre el ujo cout especicando el dato cuyo valor se mostrar. Por ejemplo:
cout << "Introduce la cantidad (en euros): " ;

escribir en la salida estndar el mensaje correspondiente a la cadena de caracteres especicada. El siguiente ejemplo escribe en la salida estndar el valor de las variables euros y pesetas, as como un mensaje para interpretarlos adecuadamente. El smbolo endl especica que la sentencia deber escribir un n de lnea, que indica que lo que se muestre a continuacin se realizar en una nueva lnea.
cout << euros << " Euros equivalen a " << pesetas << " Pts" << endl ;

Salida de Datos Formateada


Cuando se utiliza en operador de salida << para escribir un dato, el resultado que aparece en la salida se ajusta a un comportamiento por defecto. En ocasiones nos puede interesar controlar la forma en la que se muestran los datos, especicando un determinado formato. Para ello, es necesario incluir la biblioteca estndar iomanip. Por ejemplo, en el siguiente programa usamos algunos manipuladores tpicos:
#include <iostream> #include <iomanip> using namespace std ; int main() { bool x = true ; cout << boolalpha << x ; // escribe los booleanos como false o true cout << dec << 27 ; // escribe 27 (decimal) cout << hex << 27 ; // escribe 1b (hexadecimal) cout << oct << 27 ; // escribe 33 (octal) cout << setprecision(2) << 4.567 ; // escribe 4.6 cout << setw(5) << 234 ; // escribe " 234" cout << setfill(#) << setw(5) << 234 ; // escribe "##234" }

cuyo comportamiento es el siguiente: El manipulador boolalpha especica que los valores lgicos se mostrarn mediante los valores false y true. Si no se especica, se muestran los valores 0 y 1 respectivamente. Los manipuladores dec, hex, oct especican que la salida se realizar utilizando el sistema de numeracin decimal, hexadecimal u octal respectivamente. El manipulador setprecision(...) especica la cantidad de dgitos signicativos (precisin) que se mostrar en la salida de nmeros reales. El manipulador setw(...) especica la anchura (width ) que como mnimo ocupar la salida da datos (permite mostrar la informacin de forma tabulada). El manipulador setfill(...) especica el carcter de relleno (ll ) que se utilizar, en caso de ser necesario, para ocupar toda la anchura del campo de salida (especicada con setw). Por defecto, si no se especica el carcter de relleno, se utiliza el espacio en blanco ( ).
Dpto. Lenguajes y Ciencias de la Computacin Universidad de Mlaga

3.3. ENTRADA DE DATOS

27

3.3.

Entrada de Datos

La entrada de datos permite recibir informacin desde el exterior, y se realiza a travs de los ujos de entrada. El ujo de entrada asociado a la entrada estndar (usualmente el teclado) se denomina cin. La entrada de datos desde el teclado se realiza mediante el operador >> sobre el ujo cin especicando la variable donde almacenar el valor de entrada ledo desde el teclado:
cin >> euros ;

incluso es posible leer varios valores consecutivamente en la misma sentencia de entrada, de tal forma que las siguientes sentencias son equivalentes:
cin >> minimo ; cin >> maximo ;

es equivalente a

cin >> minimo >> maximo ;

El operador de entrada >> se comporta de la siguiente forma: elimina los espacios en blanco1 que hubiera al principio del ujo de entrada de datos, y lee de dicho ujo de entrada una secuencia de caracteres hasta que encuentre algn carcter no vlido (segn el tipo de la variable que almacenar la entrada de datos), que no ser ledo y permanecer disponible en el ujo de entrada de datos hasta que se realice la prxima operacin de entrada. La secuencia de caracteres leda del ujo de entrada ser manipulada (convertida) para obtener el valor correspondiente del tipo adecuado que ser almacenado en la variable especicada. En caso de que durante la operacin de entrada de datos surja alguna situacin de error, dicha operacin de entrada se detiene y el ujo de entrada se pondr en un estado errneo. Por ejemplo, dado el siguiente programa:
#include <iostream> using namespace std ; int main() { int num_1, num_2 ; cout << "Introduce el primer nmero: " ; cin >> num_1 ; cout << "Introduce el segundo nmero: " ; cin >> num_2 ; cout << "Multiplicacin: " << (num_1 * num_2) << endl ; cout << "Fin" << endl ; }

si al ejecutarse, el usuario introduce 12 enter como primer nmero, y 3 enter como segundo nmero, se produce la siguiente salida:
Introduce el primer nmero: Introduce el segundo nmero: Multiplicacin: 36 Fin
12 3
ENTER

ENTER

El funcionamiento detallado es el siguiente:


1. La ejecucin del programa escribe en pantalla el mensaje para que el usuario introduzca el primer nmero. 2. Al estar vaco el ujo de entrada, el operador >> detiene la ejecucin en espera de datos. Cuando el usuario introduce una entrada de datos 12 enter , el operador >> extrae del ujo de entrada cin la secuencia de caracteres 1 y 2, deteniendo la extraccin al encontrar el carcter ENTER. Estos caracteres se convierten al nmero 12 (1 * 10 + 2) que ser almacenado en la variable num_1.
1 Se consideran espacios en blanco los siguientes caracteres: espacio en blanco ( ), tabuladores (\t, \v y \f), retorno de carro (\r) y nueva lnea (\n).

Dpto. Lenguajes y Ciencias de la Computacin

Universidad de Mlaga

28

CAPTULO 3. ENTRADA Y SALIDA DE DATOS BSICA


3. Posteriormente, el programa escribe en pantalla el mensaje para que el usuario introduzca el segundo nmero. 4. En la siguiente operacin de entrada, el operador >> elimina el carcter ENTER del ujo de entrada cin, remanente de la operacin anterior. Al quedar vaco el ujo de entrada, la operacin de entrada se detiene hasta que haya nuevos caracteres disponibles. Cuando el usuario introduce una nueva entrada 3 enter , el operador >> extrae del ujo de entrada cin la secuencia de caracteres 3, deteniendo la extraccin al encontrar el carcter ENTER. Estos caracteres se convierten al nmero 3 que ser almacenado en la variable num_2. 5. Finalmente se realiza la multiplicacin de ambos nmeros, mostrando el resultado (36).

Sin embargo, si al ejecutarse de nuevo el programa, el usuario introduce 12 espacio 3 enter como primer nmero, entonces la ejecucin del programa no se detiene para permitir introducir el segundo nmero, ya que ste ya se encuentra en el ujo de entrada de datos, por lo que se produce la siguiente salida:
Introduce el primer nmero: 12 ESP 3 ENTER Introduce el segundo nmero: Multiplicacin: 36 Fin

El funcionamiento detallado es el siguiente:


1. La ejecucin del programa escribe en pantalla el mensaje para que el usuario introduzca el primer nmero. 2. Al estar vaco el ujo de entrada, el operador >> detiene la ejecucin en espera de datos. Cuando el usuario introduce una entrada de datos 12 espacio 3 enter , el operador >> extrae del ujo de entrada cin la secuencia de caracteres 1 y 2, deteniendo la extraccin al encontrar el carcter ESPACIO. Estos caracteres se convierten al nmero 12 (1 * 10 + 2) que ser almacenado en la variable num_1. 3. Posteriormente, el programa escribe en pantalla el mensaje para que el usuario introduzca el segundo nmero. 4. En la siguiente operacin de entrada, el operador >> elimina el carcter ESPACIO del ujo de entrada cin y encuentra caracteres disponibles en el ujo de entrada 3 enter , remanentes de la operacin anterior, por lo que el operador >> no detendr la ejecucin del programa, y extrae del ujo de entrada cin la secuencia de caracteres 3, deteniendo la extraccin al encontrar el carcter ENTER. Estos caracteres se convierten al nmero 3 que ser almacenado en la variable num_2. 5. Finalmente se realiza la multiplicacin de ambos nmeros, mostrando el resultado (36).

Entrada de Datos Avanzada


Hay ocasiones en la que nos interesa obtener caracteres del ujo de entrada sin saltar los espacios en blanco, ya que por razones de procesamiento, nos puede ser til el procesamiento de dichos caracteres. Por ello, el siguiente operador nos permite leer, desde el ujo de entrada, un nico carcter, sin eliminar los espacios iniciales, de tal forma que stos tambin puedan ser procesados:
{ char c ; cin.get(c) ; // lee un carcter sin eliminar espacios en blanco iniciales ... }

En caso de querer eliminar los espacios iniciales explcitamente, el manipulador ws realizar dicha operacin de eliminacin de los espacios iniciales:
{ char c ; cin >> ws ; // elimina los espacios en blanco iniciales
Universidad de Mlaga

Dpto. Lenguajes y Ciencias de la Computacin

3.4. CONTROL DEL ESTADO DEL FLUJO


cin.get(c) ; // lee sin eliminar espacios en blanco iniciales ... }

29

Es posible tambin eliminar un nmero determinado de caracteres del ujo de entrada, o hasta que se encuentre un determinado carcter:
{ cin.ignore() ; // elimina el prximo carcter cin.ignore(5) ; // elimina los 5 prximos caracteres cin.ignore(1000, \n) ; // elimina 1000 caracteres o hasta ENTER (nueva-lnea) }

La entrada y salida de cadenas de caracteres se puede ver en los captulos correspondientes (cap. 6.2.1).

3.4.

Control del Estado del Flujo

Cuando se realiza una entrada de datos errnea, el ujo de entrada se pondr en un estado de error, de tal forma que cualquier operacin de entrada de datos sobre un ujo de datos en estado errneo tambin fallar. Por ejemplo, la ejecucin del siguiente programa entrar en un bucle sin n en caso de que se introduzca una letra en vez de un nmero, ya que el valor que tome la variable n no estar en el rango adecuado, y cualquier otra operacin de entrada tambin fallar, por lo que el valor de n nunca podr tomar un valor vlido dentro del rango especicado:
int main() { int n = 0; do { cout << "Introduzca un numero entre [1..9]: "; cin >> n; } while (! (n > 0 && n < 10)); cout << "Valor: " << n << endl; }

Sin embargo, es posible comprobar el estado de un determinado ujo de datos, y en caso de que se encuentre en un estado de error, es posible restaurarlo a un estado correcto, por ejemplo:
int main() { int n = 0; do { cout << "Introduzca un numero [1..9]: "; cin >> n; while (cin.fail()) { // Estado Errneo ? cin.clear(); // Restaurar estado cin.ignore(1000, \n); // Eliminar la entrada de datos anterior cout << "Error: Introduzca un numero [1..9]: "; cin >> n; } } while (! (n > 0 && n < 10)); cout << "Valor: " << n << endl; }

Dpto. Lenguajes y Ciencias de la Computacin

Universidad de Mlaga

30

CAPTULO 3. ENTRADA Y SALIDA DE DATOS BSICA

Dpto. Lenguajes y Ciencias de la Computacin

Universidad de Mlaga

Captulo 4

Estructuras de Control
El lenguaje de programacin C++ dispone de estructuras de control muy exibles. Aunque ello es positivo, un abuso de la exibilidad proporcionada por el lenguaje puede dar lugar a programas con estructuras complejas y caractersticas no deseables. Por ello, slo veremos algunas de ellas y las utilizaremos en contextos y situaciones restringidas. Todas aquellas estructuras que no se presenten en este texto no sern utilizadas en el curso.

4.1.

Sentencia, Secuencia y Bloque

En C++ la unidad fundamental de accin es la sentencia, y expresamos la composicin de sentencias como una secuencia de sentencias terminadas por el carcter punto y coma (;). Su orden de ejecucin (ujo de ejecucin ) es secuencial , es decir, cada sentencia se ejecuta cuando termina la anterior, siguiendo el orden en el que estn escritas en el texto del programa. Un bloque es una unidad de ejecucin mayor que la sentencia, y permite agrupar una secuencia de sentencias como una unidad. Para ello, formaremos un bloque delimitando entre dos llaves la secuencia de sentencias que agrupa. Es posible anidar bloques, es decir, se pueden denir bloques dentro de otros bloques.
int main() { <sentencia_1> ; <sentencia_2> ; { <sentencia_3> ; <sentencia_4> ; . . . } <sentencia_n> ; }

4.2.

Declaraciones Globales y Locales

En el captulo 2 tratamos acerca del mbito de visibilidad de los identicadores y lo aplicamos a nuestro ejemplo bsico, haciendo referencia a la declaracin de constantes y variables. En este captulo utilizaremos bloques que, como hemos visto, pueden estar anidados dentro de otros bloques. Ello introduce la posibilidad de declarar identicadores en diferentes bloques, pudiendo entrar en conicto unos con otros, por lo que debemos conocer las reglas utilizadas en el lenguaje C++ para decidir a qu identicador nos referimos en cada caso. Distinguiremos dos clases de declaraciones: globales y locales. Entidades globales son aquellas que han sido denidas fuera de cualquier bloque. Su mbito de visibilidad comprende desde el punto en el que se denen hasta el nal del chero. Este tipo 31

32

CAPTULO 4. ESTRUCTURAS DE CONTROL

de entidades se crean al principio del programa y se destruyen al nalizar ste. Por ejemplo, la constante simblica EUR_PTS, utilizada en nuestro primer programa, es global y visible (utilizable) desde el punto en que se declar hasta el nal del chero. En captulos posteriores estudiaremos otras entidades que se declaran con un mbito global, como tipos denidos por el programador (ya hemos visto tipos enumerados), prototipos de subprogramas y deniciones de subprogramas. Tambin es posible denir variables con un mbito global, aunque es una prctica que, en general, est desaconsejada y no seguiremos en este curso. Entidades locales son aquellas que se denen dentro de un bloque. Su mbito de visibilidad comprende desde el punto en el que se denen hasta el nal de dicho bloque. Este tipo de entidades se crean en el punto donde se realiza la denicin, y se destruyen al nalizar el bloque. Normalmente sern variables locales, aunque tambin es posible declarar constantes localmente. Sin embargo, por lo general en este curso seguiremos el criterio de declarar las constantes globalmente escribiendo su denicin al principio del programa, fuera de cualquier bloque.
#include <iostream> using namespace std ; const double EUR_PTS = 166.386 ; // Declaracin de constante GLOBAL int main() { cout << "Introduce la cantidad (en euros): " ; double euros ; // Declaracin de variable LOCAL cin >> euros ; double pesetas = euros * EUR_PTS ; // Declaracin de variable LOCAL cout << euros << " Euros equivalen a " << pesetas << " Pts" << endl ; }

En caso de tener declaraciones de diferentes entidades con el mismo identicador en diferentes niveles de anidamiento, la entidad visible ser aquella que se encuentre declarada en el bloque de nivel de anidamiento ms interno. La declaracin ms interna oculta a aquella ms externa. Sin embargo, se considera que no es una buena prctica de programacin ocultar identicadores al redenirlos en niveles de anidamiento ms internos, ya que ello conduce a programas difciles de leer y propensos a errores, por lo que procuraremos evitar esta prctica. En el siguiente ejemplo declaramos una variable local x de tipo int y la inicializamos con el valor 3. Posteriormente declaramos una nueva variable x, diferente de la anterior, de tipo double y con valor inicial 5.0. En el bloque interno coexisten ambas variables en memoria, cada una de tipo diferente, pero cualquier uso del identicador x har referencia a la variable de tipo double, porque est declarada en un mbito ms interno, ocultando as a la variable x de tipo int.
int main() { int x = 3 ; int z = x * 2 ; // x es vble de tipo int con valor 3 { double x = 5.0 ; double n = x * 2 ; // x es vble de tipo double con valor 5.0 } int y = x + 4 ; // x es vble de tipo int con valor 3 }

4.3.

Sentencias de Seleccin

Hasta ahora hemos considerado instrucciones de asignacin y de entrada/salida, y el ujo de ejecucin es secuencial. Sin embargo, este tipo de instrucciones no basta si necesitamos programas en los que queremos optar entre diferentes comportamientos, dependiendo de diferentes condiciones. Por ejemplo, podramos estar interesados en disponer de un programa de conversin de monedas que funcione, tanto para convertir de euros a pesetas como al contrario. En este caso, el usuario informara en primer lugar del tipo de conversin que desea y posteriormente se aplicara una
Dpto. Lenguajes y Ciencias de la Computacin Universidad de Mlaga

4.3. SENTENCIAS DE SELECCIN

33

conversin u otra. Para poder describir este comportamiento necesitamos sentencias de seleccin, que permitan efectuar preguntas y seleccionar el comportamiento adecuado en funcin del resultado de las mismas. Las preguntas sern expresadas mediante expresiones lgicas, que devuelven un valor de tipo bool. El valor resultante de evaluar la expresin lgica (true o false), ser utilizado para seleccionar el bloque de sentencias a ejecutar, descartando el resto de alternativas posibles. Sentencia if La sentencia de seleccin se puede utilizar con diferentes formatos. Para el ejemplo mencionado usaramos una sentencia de seleccin condicional compuesta:
if ( <expresin_lgica> ) { <secuencia_de_sentencias_v> ; } else { <secuencia_de_sentencias_f> ; }
false true

Cond

Acciones

Acciones

cuya ejecucin comienza evaluando la expresin lgica. Si su resultado es verdadero (true) entonces se ejecuta la <secuencia_de_sentencias_v> . Sin embargo, si su resultado es falso (false), entonces se ejecuta la <secuencia_de_sentencias_f> . Posteriormente la ejecucin contina por la siguiente intrucccin despus de la sentencia if. De esta forma, podramos obtener el siguiente programa para convertir de pesetas a euros o viceversa, dependiendo del valor introducido por el usuario.
#include <iostream> using namespace std ; const double EUR_PTS = 166.386 ; int main() { char resp ; cout << "Teclee P para convertir a Pesetas y E para convertir a Euros: " ; cin >> resp ; cout << "Introduce la cantidad : " ; double cantidad, result ; cin >> cantidad ; if (resp == P) { result = cantidad * EUR_PTS ; } else { result = cantidad / EUR_PTS ; } cout << cantidad << " equivale a " << result << endl ; }

En determinadas ocasiones puede resultar interesante que el programa se plantee elegir entre ejecutar un grupo de sentencias o no hacer nada. En este caso la rama else puede ser omitida, obteniendo una sentencia de seleccin simple, que responde al siguiente esquema:
true

Cond

if ( <expresin_lgica> ) { <secuencia_de_sentencias> ; }

false

Acciones

cuya ejecucin comienza evaluando la expresin lgica. Si su resultado es verdadero (true), entonces se ejecuta la secuencia de sentencias entre llaves; si es falso (false), entonces no se ejecuta ninguna
Dpto. Lenguajes y Ciencias de la Computacin Universidad de Mlaga

34

CAPTULO 4. ESTRUCTURAS DE CONTROL

sentencia. Como ejemplo, a continuacin mostramos un programa que lee tres nmeros por teclado e imprime el valor mayor:
#include <iostream> using namespace std ; int main () { int a, b, c ; cin >> a >> b >> c ; int mayor = a ; if (b > mayor) { mayor = b ; } if (c > mayor) { mayor = c ; } cout << mayor << endl ; }

Es frecuente encontrar situaciones en las que se desea seleccionar una alternativa entre varias. En este caso se pueden encadenar varias sentencias de seleccin:
true

if ( <expresin_lgica_1> ) { <secuencia_de_sentencias_v1> ; } else if ( <expresin_lgica_2> ){ <secuencia_de_sentencias_v2> ; } else if ... ... } else { <secuencia_de_sentencias_f> ; }

Cond
false true

Acciones

Cond
false

Acciones

...
true

...
Acciones

Cond
false

Acciones

En este caso el comportamiento es el que se podra esperar intuitivamente. En primer lugar se evala la primera expresin lgica <expresion_logica_1> . Si el resultado es true se ejecuta la secuencia de sentencias <secuencia_de_sentencias_v1> y se descartan el resto de alternativas. Si el resultado es false se procede de igual manera con la siguiente expresin lgica <expresion_logica_2> , y as sucesivamente. La ltima rama, correspondiente a else, es opcional: si todas las expresiones lgicas fallan y aparece una rama else nal, se ejecutan las sentencias <secuencia_de_sentencias_f> , pero que si no aparece, al no haber sentencias a ejecutar, el efecto es no ejecutar nada, continuando el ujo de control por la siguiente instruccin. Como ejemplo, el siguiente programa lee de teclado un nmero que representa una nota numrica entre cero y diez. Si es correcta queremos mostrar su equivalente en formato textual, y si es incorrecta queremos mostrar un mensaje de error.
#include <iostream> using namespace std ; int main () { double nota ; cin >> nota ; if ( ! ((nota >= 0.0) && (nota <= 10.0))) { cout << "Error: 0 <= n <= 10" << endl ; } else if (nota >= 9.5) { cout << "Matrcula de Honor" << endl ; } else if (nota >= 9.0) { cout << "Sobresaliente" << endl ;
Dpto. Lenguajes y Ciencias de la Computacin Universidad de Mlaga

4.3. SENTENCIAS DE SELECCIN


} else if (nota >= 7.0) { cout << "Notable" << endl ; } else if (nota >= 5.0) { cout << "Aprobado" << endl ; } else { cout << "Suspenso" << endl ; } }

35

Sentencia switch Aunque el uso de la sentencia if podra ser suciente para seleccionar ujos de ejecucin alternativos, hay numerosas situaciones que podran quedar mejor descritas utilizando otras estructuras de control que se adapten mejor. Por ejemplo, supongamos que queremos ampliar nuestro programa de conversin de moneda para ofrecer un men de opciones que permita convertir diferentes monedas (pesetas, francos, marcos y liras) a euros. En este caso se podra haber optado por usar una sentencia de seleccin mltiple, de la siguiente forma:
#include <iostream> using namespace std ; const double EUR_PTS = 166.386 ; const double EUR_FRC = 6.55957 ; const double EUR_MRC = 1.95583 ; const double EUR_LIR = 1936.27 ; int main() { char resp ; cout << "Teclee P para convertir cout << "Teclee F para convertir cout << "Teclee M para convertir cout << "Teclee L para convertir cout << "Opcion: " ; cin >> resp ; cout << "Introduce la cantidad : double cantidad, result ; cin >> cantidad ; if (resp == P){ result = cantidad * EUR_PTS }else if (resp == F){ result = cantidad * EUR_FRC }else if (resp == M){ result = cantidad * EUR_MRC }else { // Si no es ninguna de result = cantidad * EUR_LIR }

de de de de

Pesetas a Euros" ; Francos a Euros" ; Marcos a Euros" ; Liras a Euros" ;

" ;

; ; ; las anteriores es a Liras ;

cout << cantidad << " equivale a " << result << endl ; }

Se trata de una situacin en la que la secuencia de sentencias alternativas a ejecutar se decide en funcin del valor que tome una determinada expresin. En el ejemplo, la expresin es directamente una variable cuyo valor determina qu rama ejecutar. Para estas situaciones se dispone de la sentencia switch, cuyo uso hace que el programa quede ms claro y, por tanto, mejor. La sentencia switch tiene el siguiente formato y se utiliza en situaciones en que la expresin es de tipo ordinal (vase 2.2), y exige que los valores que se utilizan para seleccionar cada rama sean constantes.
Dpto. Lenguajes y Ciencias de la Computacin Universidad de Mlaga

36
switch ( <expresin> ) { case <valor_cte_1> : <secuencia_de_sentencias_1> break ; case <valor_cte_2> : case <valor_cte_3> : <secuencia_de_sentencias_2> break ; case <valor_cte_4> : <secuencia_de_sentencias_3> break ; . . . default: <secuencia_de_sentencias_d> break ; }

CAPTULO 4. ESTRUCTURAS DE CONTROL

;
case_1 case_2

Expr

...

case_n

default

Acciones

Acciones
break

Acciones
break

Acciones
break

break

Al ejecutar una sentencia switch, en primer lugar se evala la expresin de control. A continuacin, se comprueba si el resultado coincide con alguno de los valores utilizados para seleccionar las diferentes ramas, comenzando por la primera. El ujo de ejecucin contina por aquella rama en la que se encuentre un valor que coincida con el valor resultado de evaluar la expresin de control. Si el valor de la expresin no coincide con ningn valor especicado, se ejecuta la secuencia de sentencias correspondiente a la etiqueta default (si es que existe). Aunque existen otras posibilidades, nosotros utilizaremos la sentencia switch en situaciones en las que queremos ejecutar una rama u otra de forma excluyente, por lo que despus de la secuencia de sentencias asociada a cada rama siempre usaremos una sentencia break, cuya ejecucin hace que el ujo de control pase a la sentencia del programa que se encuentra a continuacin de la sentencia switch. En nuestro ejemplo de conversin de mltiples monedas, podramos haber escrito el programa usando switch de la siguiente forma:
#include <iostream> using namespace std ; const double EUR_PTS = 166.386 ; const double EUR_FRC = 6.55957 ; const double EUR_MRC = 1.95583 ; const double EUR_LIR = 1936.27 ; int main() { char resp ; cout << "Teclee P para convertir cout << "Teclee F para convertir cout << "Teclee M para convertir cout << "Teclee L para convertir cout << "Opcion: " ; cin >> resp ; cout << "Introduce la cantidad : double cantidad, result ; cin >> cantidad ;

de de de de

Pesetas a Euros" ; Francos a Euros" ; Marcos a Euros" ; Liras a Euros" ;

" ;

switch (resp){ case P: result = cantidad * EUR_PTS ; break ; case F: result = cantidad * EUR_FRC ; break ; case M: result = cantidad * EUR_MRC ; break ; case L:
Dpto. Lenguajes y Ciencias de la Computacin Universidad de Mlaga

4.4. SENTENCIAS DE ITERACIN. BUCLES


result = cantidad * EUR_LIR ; break ; } cout << cantidad << " equivale a " << result << endl ; }

37

Aunque aparentemente la versin con if es similar a la versin con switch, en realidad esta segunda versin es ms clara. Cualquier persona que lea el programa podr suponer que su intencin es seleccionar una alternativa u otra en funcin del valor resultado de evaluar una determinada expresin, mientras que para llegar a la misma conclusin en la versin con if es necesario leer una a una todas las expresiones que seleccionan la entrada en cada rama.

4.4.

Sentencias de Iteracin. Bucles

Las sentencias de iteracin se utilizan para repetir la ejecucin de un determinado grupo de sentencias dependiendo de determinadas condiciones. El ujo de ejecucin estar iterando (por eso se les conoce como bucles ), repitiendo la ejecucin del grupo de sentencias (cuerpo del bucle ) hasta que lo determine la expresin lgica utilizada para decidir la nalizacin del bucle. Un clculo complejo suele construirse en base a repetir adecuadamente pasos ms simples. Por ello la mayora de los programas harn uso, de un modo u otro, de sentencias de iteracin. Por este motivo es muy importante adquirir una destreza adecuada en el diseo de programas que hagan uso de estructuras de iteracin. Utilizaremos tres tipos de sentencias de iteracin diferentes: while, for y do-while. En numerosas ocasiones ser posible obtener la solucin a un problema usando cualquiera de ellas. Sin embargo, deberemos procurar optar por aquel tipo de sentencia que mejor se adapte a la solucin adoptada para el programa, ya que de esta forma conseguiremos programas ms claros y mejores. Sentencia while Comenzamos mostrando la sentencia while, la ms general, que responde al siguiente esquema:

false

while ( <expresin_lgica> ) { <secuencia_de_sentencias> ; }

Cond
true

Acciones

Su ejecucin comienza con la evaluacin de la expresin lgica. Si es falsa, el cuerpo del bucle no se ejecuta y el ujo de ejecucin pasa directamente a la instruccin que siga a la sentencia while. Si es cierta, el ujo de ejecucin pasa al cuerpo del bucle, el cual se ejecuta completamente en secuencia y posteriormente vuelve a evaluar la expresin lgica. Este ciclo iterativo consistente en evaluar la condicin y ejecutar las sentencias se realizar MIENTRAS que la condicin se evale a verdadero y nalizar cuando la condicin se evale a falso. Por ejemplo, supongamos que queremos que nuestro programa de conversin se ejecute repetidamente hasta que el usuario teclee N como respuesta a la operacin a realizar. En ese caso podramos utilizar el siguiente programa:
#include <iostream> using namespace std ; const double EUR_PTS = 166.386 ; int main() { char resp ;
Dpto. Lenguajes y Ciencias de la Computacin Universidad de Mlaga

38
cout << "Teclee P (a Pesetas), E (a cin >> resp ; while (res != F){ cout << "Introduce la cantidad : double cantidad, result ; cin >> cantidad ; if (resp == P){ result = cantidad * EUR_PTS ; }else{ result = cantidad / EUR_PTS ; } cout << cantidad << " equivale a cout << "Teclee P (a Pesetas), E cin >> resp ; } }

CAPTULO 4. ESTRUCTURAS DE CONTROL


Euros) o F (Fin): " ;

" ;

" << result << endl ; (a Euros) o F (Fin): " ;

A continuacin, mostramos otro ejemplo en el que, dado un nmero tomado como entrada, escribimos en pantalla el primer divisor mayor que 1. Si el nmero no es mayor que 1 esto no es posible; en ese caso mostramos 1.
#include <iostream> using namespace std ; int main () { int num, divisor ; cin >> num ; if (num <= 1) { divisor = 1 ; } else { divisor = 2 ; while ((num % divisor) != 0) { ++divisor ; } } cout << "El primer divisor de " << num << " es " << divisor << endl ; }

Como puede observarse en estos ejemplos, el nmero de iteraciones del bucle depende de cuando se evale a false la expresin lgica que, a su vez, depende de variables utilizadas en el bucle. Por ello, habr que considerar qu variables utilizamos en la expresin de control y de qu forma se comportan. Para que el bucle se ejecute exactamente el nmero de iteraciones que nos interesa, es importante que nos aseguremos de que dichas variables tienen los valores adecuados inicialmente y que estudiemos cmo son modicadas en el cuerpo del bucle. Si las variables nunca fueran modicadas en el cuerpo tendramos un bucle innito, porque la condicin de control nunca se cumplira y por tanto el programa nunca terminara. Sentencia for Frecuentemente nos encontramos con situaciones en las que el nmero de iteraciones que deseamos que ejecute el bucle es previsible y puede ser controlado utilizando una variable de control. En estos casos la solucin queda ms clara si utilizamos un tipo de bucle que permita conocer fcilmente qu variable se utiliza para controlar cuando acaba la ejecucin, y cmo va evolucionando. Para ello, en C++ se utiliza la sentencia for, similar a la de otros lenguajes como Pascal o Modula-2, pero mucho ms exible, por lo que habr que utilizarla aprovechando su exibilidad pero sin abusar, porque ello podra producir programas inadecuados. En realidad la sentencia for puede ser vista como una construccin especializada de la sentencia while presentada anteriormente, pero con una sintaxis diferente para hacer ms explcito los casos
Dpto. Lenguajes y Ciencias de la Computacin Universidad de Mlaga

4.4. SENTENCIAS DE ITERACIN. BUCLES

39

en los que la iteracin est controlada por los valores que toma una determinada variable de control, de tal forma que existe una clara inicializacin y una clara modicacin (incremento) de la variable de control, hasta llegar al caso nal. La sentencia for sigue el siguiente esquema:
for ( <inicializacin> ; <expresin_lgica> ; <incremento> ) { <secuencia_de_sentencias> ; } y es equivalente a: <inicializacin> ; while ( <expresin_lgica> ) { <secuencia_de_sentencias> ; <incremento> ; }
Inicializac

false

Cond
true

Acciones

Incremento

Para utilizar adecuadamente la estructura for es necesario que el comportamiento iterativo del bucle quede claramente especicado utilizando nicamente los tres componentes (inicializacin, condicin de n e incremento) de la cabecera de la sentencia. De esta forma, el programador podr conocer el comportamiento del bucle sin necesidad de analizar el cuerpo del mismo. As, nunca debemos modicar la variable de control de un bucle for dentro del cuerpo del mismo. Nota: es posible y adecuado declarar e inicializar la variable de control del bucle en el lugar de la inicializacin. En este caso especial, el mbito de visibilidad de la variable de control del bucle es solamente hasta el nal del bloque de la estructura for. Como ejemplo, a continuacin mostramos un programa que toma como entrada un nmero n y muestra por pantalla la serie de nmeros 0 1 2 . . . n.
#include <iostream> using namespace std ; int main () { int n ; cin >> n ; for (int i = 0 ; i < n ; ++i) { cout << i << " " ; } // i NO es visible aqu cout << endl ; }

Ya hemos visto cmo la sentencia for puede ser considerada como una especializacin de la sentencia while, por lo que el programa anterior tambin podra haber sido descrito en base a la sentencia while de la siguiente forma:
#include <iostream> using namespace std ; int main () { int n ; cin >> n ; int i = 0; while (i < n) { cout << i << " " ; ++i; } // i SI es visible aqu cout << endl ; }
Dpto. Lenguajes y Ciencias de la Computacin Universidad de Mlaga

40

CAPTULO 4. ESTRUCTURAS DE CONTROL

Ambas soluciones son correctas y a primera vista puede parecer que de similar claridad. Sin embargo, la primera solucin es mejor que la segunda: en el primer caso toda la informacin necesaria para analizar el comportamiento del bucle est claramente localizable, mientras que en el segundo es necesario localizarla estudiando cmo se inicializa la variable y cmo se incrementa. Aunque en este ejemplo ello est claro, piense que en otras situaciones esta informacin puede estar mucha ms oculta y, por tanto, ser ms difcil de obtener. Sentencia do-while Finalmente, se dispone de un tercer tipo de sentencia de iteracin, la sentencia do-while. Al igual que ocurre con la sentencia for, esta sentencia puede ser vista como una construccin especializada para determinadas situaciones que, aunque admiten una solucin en base a la sentencia while, presentan una solucin ms clara con do-while. La sentencia do-while sigue el siguiente esquema:

do { <secuencia_de_sentencias> ; } while ( <expresin_lgica> ) ;

Acciones

Cond
true false

En este caso, a diferencia de la sentencia while, en la que primero se evala la expresin lgica y despus, en caso de ser cierta, se ejecuta la secuencia de sentencias, en la estructura do-while primero se ejecuta la secuencia de sentencias y posteriormente se evala la expresin lgica y, si sta es cierta, se repite el proceso. En este caso, el ujo de ejecucin alcanza la expresin lgica despus de ejecutar el cuerpo del bucle al menos una vez. Ello hace que se utilice para situaciones en las que sabemos que siempre queremos que el cuerpo del bucle se ejecute al menos una vez. Por ejemplo, si queremos disear un programa que lea un nmero y se asegure de que es par, repitiendo la lectura en caso de no serlo, podramos hacer lo siguiente:
#include <iostream> using namespace std; int main () { int num ; do { cout << "Tecle un nmero par: " ; cin >> num ; } while ((num % 2) != 0) ; cout << "El nmero par es " << num << endl ; }

Como hemos mencionado, tambin sera posible obtener una versin alternativa de este programa haciendo uso de una sentencia while, de la siguiente forma:
#include <iostream> using namespace std; int main () { int num ; cout << "Tecle un nmero par: " ; cin >> num ; while ((num % 2) != 0){ cout << "Tecle un nmero par: " ; cin >> num ; }
Dpto. Lenguajes y Ciencias de la Computacin Universidad de Mlaga

4.5. PROGRAMACIN ESTRUCTURADA


cout << "El nmero par es " << num << endl ; }

41

Sin embargo, como puede observarse, la primera versin se adapta mejor al problema a resolver. Si ya sabemos que el cuerpo del bucle se va a repetir al menos una vez, por qu no utilizar un tipo de sentencia de iteracin que ya lo expresa? El segundo programa se basa en sacar una iteracin fuera del bucle y utilizar un tipo de bucle que se repite cero o ms veces. Aunque el comportamiento es equivalente, la solucin parece un poco forzada.

4.5.

Programacin Estructurada

Un programa sigue una metodologa de programacin estructurada si todas las estructuras de control que se utilizan (secuencia, seleccin, iteracin y modularizacin) tienen un nico punto de entrada y un nico punto de salida. Esta caracterstica hace posible que se pueda aplicar la abstraccin para su diseo y desarrollo. La abstraccin se basa en la identicacin de los elementos a un determinado nivel, ignorando los detalles especicados en niveles inferiores. Un algoritmo que use tan solo las estructuras de control tratadas en este tema, se dice que est estructurado. Bohm y Jacopini demostraron que todo problema computable puede resolverse usando nicamente estas estructuras de control. sta es la base de la programacin estructurada. El diseo de programas de acuerdo al principio de programacin estructurada permite aplicar el principio de abstraccin, tanto a la hora de disear el programa como a la hora de leer programas de otros, lo cual facilita enormemente el trabajo del programador.

false

true

Acciones Acciones

Cond
case_1 case_2

Expr

...

case_n

default

Acciones

Acciones

Acciones
break

Acciones
break

Acciones
break

Acciones
break

Inicializac

false

Cond
true false

Acciones

Cond Acciones
true

Incremento

Acciones Cond
true false

Para que la aplicacin del principio sea factible, es necesario que el programador pueda conar en que cada estructura tiene un nico punto de entrada y de salida. Ello es especialmente importante cuando se trabaja con subprogramas (ver el captulo 5), pero tambin es importante al trabajar con las estructuras bsicas presentadas en este captulo. Para aclarar este punto volveremos a repasar la sentencia de iteracin for. Sabemos que su gran ventaja est en que permite escribir en una misma zona del programa toda la informacin necesaria para saber cmo evoluciona el bucle. Ello es as porque dentro de la misma sentencia se encuentra la variable de control utilizada en el mismo, su inicializacin y su modicacin, despus de cada iteracin. El programador puede leer esta zona del programa y conocer cuntas iteraciones va a dar el bucle, utilizando la abstraccin, ignorando el cuerpo del bucle. En realidad, ello es as si estamos seguros de que el programador no hace ninguna modicacin de la variable de control dentro del cuerpo del bucle. Si el programador admite la posibilidad de que la variable de control sea modicada dentro del cuerpo del bucle,
Dpto. Lenguajes y Ciencias de la Computacin Universidad de Mlaga

42

CAPTULO 4. ESTRUCTURAS DE CONTROL

entonces es imposible aplicar el principio de abstraccin y, por tanto, el programador se ve forzado a leer con detalle el cuerpo del bucle para estar seguro del nmero de iteraciones que produce. Por este motivo, seguiremos como principio bsico el siguiente criterio de estilo en el diseo de nuestros programas: nunca modicaremos la variable de control dentro de un bucle for. As mismo, tampoco se podr modicar ninguna de las variables que aparecen en la condicin de nalizacin.

4.6.

Ejemplos

A continuacin, repasaremos los diferentes elementos introducidos en este captulo viendo algunos ejemplos. Ejemplo 1. Producto de dos nmeros Queremos realizar un programa que lea por teclado dos nmeros y muestre por pantalla su producto. Como queremos que este ejemplo nos sirva como ejemplo de estructuras de iteracin, supondremos que no es posible utilizar el operador de multiplicacin (*) directamente. En primer lugar necesitamos tener una solucin al problema y en segundo lugar necesitamos escribir adecuadamente nuestra solucin en C++. Aunque todos conocemos las tablas de multiplicar, tambin sabemos que una multiplicacin no es ms que una serie de sumas sucesivas de uno de los operandos tantas veces como indique el otro (n x m = m+m+m+...+m n veces). Multiplicar 3 x 2 no es ms que sumar 3 veces el nmero 2. En este momento ya tenemos la solucin y tenemos que escribirla en C++. Vemos que hay un proceso que se repite y que este proceso siempre se repite n veces. En tal caso, resulta adecuado usar un bucle for, controlado por una variable que asegure que se ejecutan exactamente n iteraciones. En cada iteracin bastar con acumular una nueva suma del valor m al total. Como resultado de todo ello, obtenemos el siguiente programa:
#include <iostream> using namespace std ; int main () { cout << "Introduzca dos nmeros: " ; int m, n ; cin >> m >> n ; int total = 0 ; for (int i = 0 ; i < n ; ++i) { // Proceso iterativo: acumular el valor de m al total total = total + m ; // total += m ; } cout << total << endl ; }

Ejemplo 2. Factorial A continuacin queremos obtener un programa para calcular el factorial de un nmero dado como entrada (n! = 1 * 2 * ... * n). De nuevo se trata de un problema similar al anterior: hay un proceso iterativo, en el que conocemos el nmero de iteraciones a realizar y un procesamiento acumulativo, en el que en cada iteracin multiplicamos por un valor diferente. En este caso, como tambin conocemos las iteraciones que queremos que ejecute el bucle, volvemos a usar una sentencia for, pero en esta ocasin hacemos que la variable de control i evolucione conteniendo valores que puedan ser aprovechados dentro del cuerpo del bucle, haciendo que su rango de valores vaya desde 2 hasta n. Ntese que en esta ocasin utilizamos el valor de la variable de control dentro del clculo iterativo.
Dpto. Lenguajes y Ciencias de la Computacin Universidad de Mlaga

4.6. EJEMPLOS
#include <iostream> using namespace std ; int main () { cout << "Introduzca un nmero: " ; int n ; cin >> n ; // Multiplicar: 1 2 3 4 5 6 7 ... n int fact = 1 ; for (int i = 2 ; i <= n ; ++i) { // Proceso iterativo: acumular el valor de i al total fact = fact * i ; // fact *= i ; } cout << fact << endl ; }

43

Ejemplo 3. Divisin entera Finalmente, mostraremos un programa que, dados dos nmeros tomados como entrada, muestra por pantalla su divisin entera (cociente y resto). De nuevo, en este programa evitaremos utilizar los operadores predenidos disponibles en C++ para ello (/ y %). En su lugar, aprovecharemos que la divisin entera no es ms que el resultado de una serie de restas sucesivas del divisor al dividendo, hasta que no sea posible continuar. Nuestro programa comprueba que el divisor no es cero, mostrando un mensaje de error en caso contrario. Usamos una sentencia while porque, a diferencia con lo que ocurra en el ejercicio del producto de dos nmeros, ahora no sabemos cuntas iteraciones se necesitarn para resolver el problema.
#include <iostream> using namespace std ; int main () { cout << "Introduzca dos nmeros: " ; int dividendo, divisor ; cin >> dividendo >> divisor ; if (divisor == 0) { cout << "El divisor no puede ser cero" << endl ; } else { int resto = dividendo ; int cociente = 0 ; while (resto >= divisor) { resto -= divisor ; ++cociente ; } cout << cociente << " " << resto << endl ; } }

Pruebe a modicar el programa anterior para que siempre sea posible realizar la divisin. Para ello, modique el proceso de entrada de forma que se asegure de que el dividendo es mayor o igual que cero y el divisor es mayor que cero. Si alguna de las dos cosas no ocurre, repita la toma de datos hasta que ambos datos sean correctos.

Dpto. Lenguajes y Ciencias de la Computacin

Universidad de Mlaga

44

CAPTULO 4. ESTRUCTURAS DE CONTROL

Dpto. Lenguajes y Ciencias de la Computacin

Universidad de Mlaga

Captulo 5

Subprogramas. Funciones y Procedimientos


El diseo de un programa es una tarea compleja, por lo que deberemos abordarla siguiendo un enfoque que permita hacerla lo ms simple posible. Al igual que ocurre en otros contextos, la herramienta fundamental para abordar la solucin de problemas complejos es la abstraccin : una herramienta mental que nos permite tratar el problema identicando sus elementos fundamentales y dejando para ms adelante el estudio de los detalles secundarios. La aplicacin de este principio al desarrollo de programas permite seguir un enfoque de renamientos sucesivos: en cada fase del diseo del programa ignoramos los detalles secundarios y nos centramos en lo que nos interesa en ese momento; en fases posteriores abordamos los detalles que hemos ignorado por el momento. De esta forma, al nal tenemos un diseo completo, obtenido con menor esfuerzo y de forma ms segura. Los lenguajes de programacin ofrecen la posibilidad de denir subprogramas, permitiendo al programador aplicar explcitamente la abstraccin en el diseo y construccin de software. Un subprograma puede ser visto como un mini programa encargado de resolver un subproblema, que se encuentra englobado dentro de otro mayor. En ocasiones tambin pueden ser vistos como una ampliacin del conjunto de operaciones bsicas del lenguaje de programacin, proporcionndole nuevas herramientas no disponibles de forma predenida.

5.1.

Funciones y Procedimientos

Un subprograma codica la solucin algortmica a un determinado problema. Cuando en un determinado momento de la computacin es necesario resolver dicho problema se hace uso de dicho subprograma mediante una invocacin (llamada ) al mismo. Por lo general, la resolucin del problema requerir proporcionar al subprograma la informacin necesaria para abordar su solucin (de entrada al subprograma), y es posible que el subprograma responda con algn resultado (de salida del subprograma). Para ello, un subprograma se comunica con el que lo invoca (llama) mediante parmetros. Podemos distinguir dos tipos de subprogramas: Procedimientos : encargados de resolver un problema computacional general. En el siguiente ejemplo se dispone de dos variables x e y, con determinados valores y hacemos una invocacin al procedimiento ordenar para conseguir que, como resultado, el menor de dichos nmeros quede almacenado en la variable x y el mayor en la variable y. Como vemos, en este momento no nos preocupa qu hay que hacer para que la ordenacin realmente tenga efecto, estamos ignorando esos detalles. Suponemos que el subprograma ordenar se encargar de ello. El lenguaje C++ no dispone del operador ordenar, por lo que en algn momento del diseo deberemos renar esta solucin y proporcionar una denicin para dicho subprograma. 45

46

CAPTULO 5. SUBPROGRAMAS. FUNCIONES Y PROCEDIMIENTOS


int main() { int x = 8 ; int y = 4 ; ordenar(x, y) ; cout << x << " " << y << endl ; }

Como vemos, la funcin main se comunica con el subprograma ordenar mediante una llamada. En esta comunicacin hay un intercambio de informacin entre main y ordenar a travs de los parmetros utilizados en la llamada, en este caso las variables x e y. Funciones : encargadas de realizar un clculo computacional y generar un nico resultado. Las funciones responden a los mismos principios que los procedimientos, salvo que estn especializados para que la comunicacin entre el que hace la invocacin y la funcin llamada tenga lugar de una forma especial, que se adapta muy bien y es muy til en numerosas situaciones. En el siguiente ejemplo, la funcin calcular_menor recibe dos nmeros como parmetros y calcula el menor de ellos. En este caso la comunicacin entre el que hace la llamada (la funcin main) y la funcin llamada se hace de forma diferente. Antes hicimos la invocacin en una instruccin independiente, sin embargo, ahora se hace como parte de una instruccin ms compleja. Ello es as porque una funcin devuelve un valor (en este caso el menor nmero) y dicho valor deber ser utilizado como parte de un clculo ms complejo. En nuestro ejemplo, como resultado de la invocacin obtendremos un nmero que ser almacenado en una variable de la funcin main.
int main() { int x = int y = int z = cout << }

8 ; 4 ; calcular_menor(x, y) ; "Menor: " << z << endl ;

En los ejemplos planteados vemos que tanto procedimientos como funciones se utilizan para realizar un clculo, ignorando los detalles y, por tanto, simplicando el diseo del programa principal. En ambos casos hay una invocacin al subprograma correspondiente y un intercambio de informacin entre el que llama y el subprograma llamado. La nica diferencia entre ambos tipos de subprogramas est en la forma de hacer las llamadas: La llamada a un procedimiento constituye por s sola una sentencia independiente que puede ser utilizada como tal en el cuerpo de otros subprogramas (y del programa principal). La nica forma de intercambiar informacin es a travs de los parmetros usados en la llamada. La llamada a una funcin no constituye por s sola una sentencia, por lo que debe aparecer dentro de alguna sentencia que utilice el valor resultado de la funcin. La informacin se intercambia a travs de los parmetros y mediante el valor devuelto por la funcin.

5.2.

Denicin de Subprogramas

Si intentramos compilar los programas utilizados anteriormente como ejemplo, comprobaramos que el compilador nos informa de un mensaje de error. Ello es as porque no sabe a qu nos referimos al hacer uso de ordenar y de calcular_menor. Se trata de dos identicadores nuevos, que no se encuentran predenidos en C++ y que, por lo tanto, deben ser declarados antes de su uso, como ocurre con cualquier identicador nuevo. Ntese que ambos nombres de subprogramas corresponden a tareas que el programador supone, pero cuya solucin no ha sido descrita an en ninguna parte del programa. Por este motivo, es necesario proporcionar una denicin precisa de los subprogramas ordenar y calcular_menor que, adems, debe estar situada en un punto del
Dpto. Lenguajes y Ciencias de la Computacin Universidad de Mlaga

5.2. DEFINICIN DE SUBPROGRAMAS

47

programa que la haga visible en el punto en que se usen. Por ahora situaremos la denicin de los subprogramas antes de su uso, aunque en la seccin 5.7 mostraremos otras posibilidades. Hemos comentado que un subprograma es como un mini programa encargado de resolver un subproblema, por lo que la denicin de un subprograma no diere de la denicin ya utilizada de la funcin main, que en realidad no es ms que una funcin especial. A continuacin se muestra un programa completo con la denicin de la funcin main, que dene el comportamiento del programa principal, y las deniciones de los dos subprogramas ordenar y calcular_menor, utilizados desde la funcin main.
#include <iostream> using namespace std ; int calcular_menor(int a, int b) { int menor ; if (a < b) { menor = a ; } else { menor = b ; } return menor ; } void ordenar(int& a, int& b) { if (a > b) { int aux = a ; a = b ; b = aux ; } } int main() { int x = 8 ; int y = 4 ; int z = calcular_menor(x, y) ; cout << "Menor: " << z << endl ; ordenar(x, y) ; cout << x << " " << y << endl ; }

La denicin de un subprograma consta de un encabezamiento y de un cuerpo. En el encabezamiento se especica su nombre, su tipo y los parmetros con los que dicho subprograma se comunica con el exterior. En el cuerpo se describe la secuencia de acciones necesarias para conseguir realizar la tarea que tiene asignada. El encabezamiento comienza con el nombre del tipo devuelto por el subprograma. Dicho tipo ser void si se trata de un procedimiento, ya que en dicho tipo de subprogramas no se devuelve ningn valor. Si se trata de una funcin, dicho tipo ser el correspondiente al valor devuelto por la misma. La denicin de un subprograma se hace de forma independiente de las llamadas en las que se haga uso del mismo. Por este motivo, debe contener la declaracin de todos los elementos que use. En el caso de los parmetros, habr que especicar el nombre con el que nos referimos a ellos en el cuerpo del subprograma y su tipo. El cuerpo del subprograma especica la secuencia de acciones a ejecutar necesarias para resolver el subproblema especicado, y podr denir tantas variables locales como necesite para desempear su misin. En el caso de una funcin, el valor que devuelve (el valor que toma tras la llamada) vendr dado por el resultado de evaluar la expresin de la sentencia return. Aunque C++ es ms exible, nosotros slo permitiremos una nica utilizacin de la sentencia return y deber ser al nal del cuerpo de la funcin. As mismo, un procedimiento no tendr ninguna sentencia return en su cuerpo.
Dpto. Lenguajes y Ciencias de la Computacin Universidad de Mlaga

48

CAPTULO 5. SUBPROGRAMAS. FUNCIONES Y PROCEDIMIENTOS

5.3.

Ejecucin de Subprogramas

Cuando se produce una llamada (invocacin) a un subprograma: 1. Se establecen las vas de comunicacin entre los algoritmos llamante y llamado por medio de los parmetros. 2. Posteriormente, el ujo de ejecucin pasa a la primera sentencia del cuerpo del subprograma llamado, cuyas instrucciones son ejecutadas secuencialmente, en el orden en que estn escritas. 3. Cuando sea necesario, se crean las variables locales especicadas en el cuerpo del subprograma. 4. Cuando naliza la ejecucin del subprograma, las variables locales y los parmetros previamente creados se destruyen, el ujo de ejecucin retorna al (sub)programa llamante, y contina la ejecucin por la sentencia siguiente a la llamada realizada.

5.4.

Paso de Parmetros. Parmetros por Valor y por Referencia

El intercambio de informacin en la llamada a subprogramas no diere del intercambio de informacin en una comunicacin general entre dos entidades. La informacin puede uir en uno u otro sentido, o en ambos. Dicho intercambio de informacin se realiza a travs de los parmetros. Dependiendo del contexto en que se utilicen, hablaremos de parmetros formales, si nos referimos a los que aparecen en la denicin del subprograma, o de parmetros actuales (o reales), si nos referimos a los que aparecen en la llamada al subprograma. A continuacin analizaremos este intercambio de informacin, visto desde el punto de vista del subprograma llamado, y mostraremos cmo habr que denir los parmetros formales en cada caso. Sern parmetros de entrada aquellos que se utilizan para recibir la informacin necesaria para realizar una computacin. Por ejemplo, los parmetros a y b de la funcin calcular_menor anterior. Los parmetros de entrada se denen mediante paso por valor (cuando son de tipos simples1 ). Ello signica que los parmetros formales son variables independientes, que toman sus valores iniciales como copias de los valores de los parmetros actuales de la llamada en el momento de la invocacin al subprograma. El parmetro actual puede ser cualquier expresin cuyo tipo sea compatible con el tipo del parmetro formal correspondiente. Se declaran especicando el tipo y el identicador asociado.
int calcular_menor(int a, int b) { int menor ; if (a < b) { menor = a ; } else { menor = b ; } return menor ; }

Denominamos parmetros de salida a aquellos que se utilizan para transferir al programa llamante informacin producida como parte de la computacin/solucin realizada por el subprograma.
1 Cuando son de tipos compuestos (vase 6) se denen mediante paso por referencia constante, que ser explicado ms adelante.

Dpto. Lenguajes y Ciencias de la Computacin

Universidad de Mlaga

5.4. PASO DE PARMETROS. PARMETROS POR VALOR Y POR REFERENCIA

49

Los parmetros de salida se denen mediante paso por referencia . Ello signica que el parmetro formal es una referencia a la variable que se haya especicado como parmetro actual en el momento de la llamada al subprograma. Ello exige que el parmetro actual correspondiente a un parmetro formal por referencia sea una variable. Cualquier accin dentro del subprograma que se haga sobre el parmetro formal se realiza sobre la variable referenciada, que aparece como parmetro actual en la llamada al subprograma. Se declaran especicando el tipo, el smbolo ampersand (&) y el identicador asociado. En el siguiente ejemplo, el procedimiento dividir recibe, en los parmetros de entrada dividendo y divisor, dos nmeros que queremos dividir para obtener su cociente y resto. Dichos resultados sern devueltos al punto de la llamada utilizando los parmetros de salida coc y resto, que son pasados por referencia.
void dividir(int dividendo, int divisor, int& coc, int& resto) { coc = dividendo / divisor ; resto = dividendo % divisor ; } int main() { int cociente ; int resto ; dividir(7, 3, cociente, resto) ; // ahora cociente valdr 2 y resto valdr 1 }

En realidad, dado que los parmetros coc y resto han sido pasados por referencia, una vez efectuada la llamada stos quedan asociados a las correspondientes variables usadas como parmetros actuales (las variables cociente, resto de la funcin main). As, cualquier modicacin en los parmetros es, en realidad, una modicacin de las variables cociente y resto de la funcin main. De esta forma, cuando acaba el subprograma conseguimos que los resultados estn en las variables adecuadas del llamante. El efecto es como si se hubiera producido una salida de resultados desde el subprograma llamado hacia el subprograma que realiz la llamada. Denominamos parmetros de entrada/salida a aquellos que se utilizan tanto para recibir informacin de entrada, necesaria para que el subprograma pueda realizar su computacin, como para devolver los resultados obtenidos de la misma. Se denen mediante paso por referencia y se declaran como se especic anteriormente para los parmetros de salida. Por ejemplo, los parmetros a y b del procedimiento ordenar son utilizados tanto de entrada como de salida. En el momento de la llamada aportan como entrada las variables que contienen los valores a ordenar y, cuando acaba el subprograma, son utilizados para devolver los resultados. Ello es as porque se denen mediante paso por referencia. De esta forma, cuando dentro del subprograma trabajamos con los parmetros formales a y b, en realidad accedemos a las variables utilizadas en la llamada. Si dentro del subprograma se intercambian los valores de los parmetros, indirectamente se intercambian tambin los valores de las variables de la llamada. Al terminar el subprograma el resultado est en las variables utilizadas en la llamada.
void ordenar(int& a, int& b) { if (a > b) { int aux = a ; a = b ; b = aux ; } }
Dpto. Lenguajes y Ciencias de la Computacin Universidad de Mlaga

50

CAPTULO 5. SUBPROGRAMAS. FUNCIONES Y PROCEDIMIENTOS

La siguiente tabla relaciona los diferentes modos de comunicacin con la forma de efectuar el paso de parmetros. Todo lo comentado hasta ahora es aplicable a tipos simples. En el captulo 6 consideraremos el paso de parmetros para el caso de tipos compuestos.
Simples P.Valor (int x) P.Ref (int& x) Tipos Compuestos P.Ref.Cte (const Persona& x) P.Ref (Persona& x)

() Entrada () Salida, ( ) E/S

Reglas a seguir en el paso de parmetros


En la llamada a un subprograma se deben cumplir las siguientes normas: El nmero de parmetros actuales debe coincidir con el nmero de parmetros formales. Cada parmetro formal se corresponde con aquel parmetro actual que ocupe la misma posicin en la llamada. El tipo del parmetro actual debe estar acorde con el tipo del correspondiente parmetro formal. Un parmetro formal de salida o entrada/salida (paso por referencia) requiere que el parmetro actual sea una variable. Un parmetro formal de entrada (paso por valor o paso por referencia constante) permite que el parmetro actual sea una variable, constante o cualquier expresin.

5.5.

Criterios de Modularizacin

No existen mtodos objetivos para determinar como descomponer la solucin de un problema en subprogramas, es una labor subjetiva. No obstante, se siguen algunos criterios que pueden guiarnos para descomponer un problema y modularizar adecuadamente. El diseador de software debe buscar un bajo acoplamiento entre los subprogramas y una alta cohesin dentro de cada uno. Acoplamiento: Un objetivo en el diseo descendente es crear subprogramas aislados e independientes. Sin embargo, debe haber alguna conexin entre los subprogramas para formar un sistema coherente. A dicha conexin se conoce como acoplamiento. Por lo tanto, maximizar la independencia entre subprogramas ser minimizar el acoplamiento. Cohesin: Hace referencia al grado de relacin entre las diferentes partes internas dentro de un mismo subprograma. Si la cohesin es muy dbil, la diversidad entre las distintas tareas realizadas dentro del subprograma es tal que posteriores modicaciones podrn resultar complicadas. Se busca maximizar la cohesin dentro de cada subprograma Si no es posible analizar y comprender un subprograma de forma aislada e independiente del resto, entonces podemos deducir que la divisin modular no es la ms adecuada.

5.6.

Subprogramas en Lnea

La llamada a un subprograma conlleva un pequeo coste debido al control y gestin de la misma que ocasiona cierta prdida de tiempo de ejecucin. Hay situaciones en las que el subprograma es tan pequeo que el coste asociado a la invocacin es superior al coste asociado a computar la solucin del mismo, de tal forma que en estas situaciones
Dpto. Lenguajes y Ciencias de la Computacin Universidad de Mlaga

5.7. DECLARACIN DE SUBPROGRAMAS. PROTOTIPOS

51

interesa eliminar el coste asociado a su invocacin. En ese caso se puede especicar que el subprograma se traduzca como cdigo en lnea en vez de como una llamada a un subprograma. Para ello se especicar la palabra reservada inline justo antes del tipo. De esta forma, se mantiene los benecios proporcionados por la abstraccin, pero se eliminan los costes asociados a la invocacin.
inline int calcular_menor(int a, int b) { return (a < b) ? a : b ; }

Este mecanismo slo es adecuado cuando el cuerpo del subprograma es muy pequeo, de tal forma que el coste asociado a la invocacin dominara respecto a la ejecucin del cuerpo del mismo.

5.7.

Declaracin de Subprogramas. Prototipos

Hemos comentado que, al igual que cualquier nuevo elemento del programa al que hagamos referencia con un identicador (constante simblica, variable o tipo), los subprogramas tambin deben ser declarados, permitiendo as que no resulten desconocidos para el compilador. La denicin de un subprograma sirve tambin como declaracin del mismo. Hasta ahora hemos seguido el criterio de denir el subprograma antes de su uso, pero tambin existe la posibilidad de efectuar una declaracin del subprograma, sin proporcionar su denicin completa. En este caso basta con declarar el prototipo del subprograma y suponer que ste ser denido posteriormente. El prototipo de un subprograma est formado por su encabezamiento (sin incluir el cuerpo del mismo), y acabado en el delimitador punto y coma (;). Una vez declarado el prototipo de un subprograma su mbito de visibilidad ser global al chero, es decir, desde el lugar en que ha sido declarado hasta el nal del chero en el que aparece.
int calcular_menor(int a, int b) ; // prototipo de calcular_menor int main() { int x = 8 ; int y = 4 ; int z = calcular_menor(x, y) ; } int calcular_menor(int a, int b) // definicin de calcular_menor { int menor ; if (a < b) { menor = a ; } else { menor = b ; } return menor ; }

5.8.

Sobrecarga de Subprogramas

Se denomina sobrecarga al uso del mismo identicador para aludir a subprogramas u operadores diferentes. Para saber a cul de los diferentes subprogramas u operadores nos estamos reriendo se utilizan los parmetros de los mismos, que debern ser diferentes, ya que en otro caso el compilador no sera capaz de determinar a cual nos referimos con una determinada llamada. A continuacin mostramos ejemplos de subprogramas imprimir que estn sobrecargados y pueden ser utilizados para imprimir nmeros de tipo int y double.
void imprimir(int x) {
Dpto. Lenguajes y Ciencias de la Computacin Universidad de Mlaga

52

CAPTULO 5. SUBPROGRAMAS. FUNCIONES Y PROCEDIMIENTOS


cout << "entero: " << x << endl ; } void imprimir(double x) { cout << "real: " << x << endl ; }

En el siguiente ejemplo la funcin media permite calcular la media de dos o de tres nmeros enteros, dependiendo de cmo se efecte la llamada.
double media(int x, return double(x } double media(int x, { return double(x } int y, int z) { + y + z) / 3.0 ; int y) + y) / 2.0 ;

5.9.

Pre-Condiciones y Post-Condiciones
Pre-condicin es un enunciado que debe ser cierto antes de la llamada a un subprograma. Especica las condiciones bajo las cuales se ejecutar dicho subprograma. Post-condicin es un enunciado que debe ser cierto tras la ejecucin de un subprograma. Especica el comportamiento de dicho subprograma. Codicar las pre/post-condiciones mediante asertos proporciona una valiosa documentacin, y tiene varias ventajas: Hace al programador explcitamente consciente de los prerrequisitos y del objetivo del subprograma. Durante la depuracin, las pre-condiciones comprueban que la llamada al subprograma se realiza bajo condiciones validas. Durante la depuracin, las post-condiciones comprueban que el comportamiento del subprograma es adecuado. Sin embargo, a veces no es posible codicarlas fcilmente.

En C++, las pre-condiciones y post-condiciones se pueden especicar mediante asertos, para los cuales es necesario incluir la biblioteca cassert. Por ejemplo:
#include <iostream> #include <cassert> using namespace std ; //--------------------------void dividir(int dividendo, int divisor, int& cociente, int& resto) { assert(divisor != 0) ; // PRE-CONDICION cociente = dividendo / divisor ; resto = dividendo % divisor ; assert(dividendo == (divisor * cociente + resto)) ; // POST-CONDICION }

Nota: en GNU GCC es posible desactivar la comprobacin de asertos mediante la siguiente directiva de compilacin:
g++ -DNDEBUG -ansi -Wall -Werror -o programa programa.cpp
Dpto. Lenguajes y Ciencias de la Computacin Universidad de Mlaga

5.10. EJEMPLO. NMEROS PRIMOS

53

5.10.

Ejemplo. Nmeros primos

Finalizaremos el captulo mostrando el diseo de un programa completo, en el que pretendemos imprimir todos los nmeros primos de un cierto intervalo, dado por dos valores ledos por teclado. En primer lugar leemos los nmeros y nos aseguramos de que denan un intervalo correcto. Para ello, hacemos uso del subprograma ordenar, que sita los nmeros de forma que el valor contenido en la primera variable sea menor que el de la segunda. Una vez que disponemos de un intervalo vlido, hacemos uso de un subprograma primos, que muestra por pantalla todos los primos de un cierto intervalo, denido por dos parmetros de entrada. El subprograma primos prueba uno a uno con todos los nmeros del intervalo comprobando si se trata de un nmero primo. La tarea de ver si un nmero es primo representa una abstraccin operacional claramente denida, por lo que conviene denir un nuevo subprograma que se encargue de dicha tarea de forma independiente de la implementacin del subprograma primos. Para ello, denimos una funcin es_primo que, dado un nmero como entrada devuelve un valor (de tipo bool) indicando si es primo (true) o no (false).
//- fichero: primos.cpp -------------------------------------------#include <iostream> using namespace std ; void ordenar(int& menor, int& mayor) { if (mayor < menor) { int aux = menor ; menor = mayor ; mayor = aux ; } } bool es_primo(int x) { unsigned i = 2; while ((i <= x/2) && ( x % i != 0)) { i++; } return (i == x/2+1) ; } void primos(int min, int max) { cout << "Nmeros primos entre " << min << " y " << max << endl ; for (int i = min ; i <= max ; ++i) { if (es_primo(i)) { cout << i << " " ; } } cout << endl ; } int main() { int min, max ; cout << "Introduzca el rango de valores " ; cin >> min >> max ; ordenar(min, max) ; primos(min, max) ; } //- fin: primos.cpp ------------------------------------------------

Dpto. Lenguajes y Ciencias de la Computacin

Universidad de Mlaga

54

CAPTULO 5. SUBPROGRAMAS. FUNCIONES Y PROCEDIMIENTOS

Dpto. Lenguajes y Ciencias de la Computacin

Universidad de Mlaga

Captulo 6

Tipos Compuestos
Los tipos compuestos surgen de la composicin y/o agregacin de otros tipos para formar nuevos tipos de mayor entidad. Existen dos formas fundamentales de crear tipos de mayor entidad: la composicin de elementos, que denominaremos Registros o Estructuras y la agregacin de elementos del mismo tipo, que se conocen como Agregados, Arreglos o mediante su nombre en ingls Arrays. Adems de estos tipos compuestos denidos por el programador, los lenguajes de programacin suelen proporcionar algn tipo adicional para representar cadenas de caracteres.

6.1.

Paso de Parmetros de Tipos Compuestos

Los lenguajes de programacin normalmente utilizan el paso por valor y el paso por referencia para implementar la transferencia de informacin entre subprogramas descrita en el interfaz. Para la transferencia de informacin de entrada, el paso por valor supone duplicar y copiar el valor del parmetro actual en el formal. En el caso de tipos simples, el paso por valor es adecuado para la transferencia de informacin de entrada, sin embargo, si el tipo de dicho parmetro es compuesto, es posible que dicha copia implique una alta sobrecarga, tanto en espacio de memoria como en tiempo de ejecucin. El lenguaje de programacin C++ permite realizar de forma eciente la transferencia de informacin de entrada para tipos compuestos mediante el paso por referencia constante. As, en el paso por referencia constante el parmetro formal es una referencia al parmetro actual especicado en la llamada, tomando as su valor, pero no puede ser modicado al ser una referencia constante, evitando de esta forma la semntica de salida asociada al paso por referencia. El paso por referencia constante suele utilizarse para el paso de parmetros de entrada con tipos compuestos, ya que evita la duplicacin de memoria y la copia del valor, que en el caso de tipos compuestos suele ser costosa. Para ello, los parmetros se declaran como se especic anteriormente para el paso por referencia, pero anteponiendo la palabra reservada const.
void imprimir(const Fecha& fech) { cout << fech.dia << (int(fech.mes)+1) << fech.anyo << endl ; } Simples P.Valor (int x) P.Ref (int& x) Tipos Compuestos P.Ref.Cte (const Persona& p) P.Ref (Persona& p)

() Entrada () Salida, ( ) E/S

Funciones que Retornan Tipos Compuestos Por la misma razn y como norma general, salvo excepciones, tampoco es adecuado que una funcin retorne un valor de tipo compuesto, debido a la sobrecarga que generalmente sto conlleva. 55

56

CAPTULO 6. TIPOS COMPUESTOS

En estos casos, suele ser ms adecuado que el subprograma devuelva el valor de tipo compuesto como un parmetro de salida mediante el paso por referencia.

6.2.

Cadenas de Caracteres en C++: el Tipo string

En gran cantidad de ocasiones surge la necesidad de trabajar con datos representados por secuencias de caracteres. Pensemos, por ejemplo, en programas que manipulan nombres de personas, direcciones, etc. Por ese motivo, todos los lenguajes de programacin consideran cmo procesar este tipo de situaciones, bien proporcionando tipos especcos para ello o proporcionando facilidades para que el programador dena tipos que permitan manipular cadenas de caracteres. El lenguaje C++ dispone de la biblioteca estndar <string>, que proporciona el tipo string para representar cadenas de caracteres de longitud nita, limitada por la implementacin. El tipo string dispone de operadores predenidos que permiten manejar cadenas de caracteres de forma muy simple e intuitiva. Tambin es posible manipular cadenas de caracteres con un estilo heredado del lenguaje C (como arrays de caracteres con terminador), pero el uso del tipo string permite denir cadenas de caracteres ms robustas y con mejores caractersticas, por lo que en este curso siempre usaremos cadenas de caracteres de tipo string. Una cadena de caracteres literal se representa mediante una sucesin de caracteres entre comillas dobles. Adems, el tipo string puede ser utilizado para denir constantes simblicas, variables o parmetros formales en los subprogramas. As mismo, es posible asignar un valor de tipo string a una variable del mismo tipo, o utilizar los operadores relacionales (==, !=, >, <, >=, <=) para comparar cadenas de caracteres. Para utilizar el tipo string es necesario incluir la biblioteca estndar <string>, as como utilizar el espacio de nombres de std. El siguiente programa muestra algunos ejemplos:
#include <iostream> #include <string> using namespace std ; const string AUTOR = "Jos Luis" ; int main() { string nombre = "Pepe" ; // ... nombre = AUTOR ; }

AUTOR:

J
0

o
1

s
2

e
3 4

L
5

u
6

i
7

s
8

nombre: nombre:

P
0

e
1

p
2

e
3

J
0

o
1

s
2

e
3 4

L
5

u
6

i
7

s
8

Si la denicin de una variable de tipo string no incluye la asignacin de un valor inicial, dicha variable tendr como valor por defecto la cadena vaca ("").

6.2.1.

Entrada y Salida de Cadenas de Caracteres

La entrada/salida de datos de tipo string sigue el mismo esquema que la entrada/salida de los tipos predenidos, explicada en el captulo 3. Se basa en el uso de los operadores >> y << sobre los ujos cin y cout. Su comportamiento es independiente del tipo de datos a leer, por lo que no hara falta incidir ms en ello, sin embargo, a continuacin presentamos algunos ejemplos que nos servirn de repaso, e introducimos algunas consideraciones especcas de la lectura datos de tipo string. La utilizacin del operador << sobre un ujo de salida cout muestra en la salida todos los caracteres que forman parte de la cadena. Por ejemplo, el siguiente cdigo muestra en pantalla "Nombre: Jos Luis":
#include <iostream> #include <string> using namespace std ; const string AUTOR = "Jos Luis" ; int main() {
Dpto. Lenguajes y Ciencias de la Computacin Universidad de Mlaga

6.2. CADENAS DE CARACTERES EN C++: EL TIPO STRING


cout << "Nombre: " << AUTOR << endl ; }

57

La utilizacin del operador >> sobre un ujo de entrada cin permite leer secuencias de caracteres y almacenarlas en variables de tipo string. Por ejemplo:
#include <iostream> #include <string> using namespace std ; int main() { string nombre ; cout << "Introduzca el nombre: " ; cin >> nombre ; cout << "Nombre: " << nombre << endl ; }

Este operador de entrada (>>) se comporta (como se especic en el captulo 3 dedicado a la entrada y salida bsica) de la siguiente forma: elimina los espacios en blanco que hubiera al principio de la entrada de datos, y lee dicha entrada hasta que encuentre algn carcter de espacio en blanco1 , que no ser ledo y permanecer en el buer de entrada (vase 3.1) hasta la prxima operacin de entrada. En caso de que durante la entrada surja alguna situacin de error, dicha entrada se detiene y el ujo de entrada se pondr en un estado errneo. Ntese que, como consecuencia de lo anterior, no es posible utilizar el operador >> para leer una cadena de caracteres que incluya algn carcter en blanco. Por ejemplo, si como entrada al programa anterior introducimos por teclado la secuencia de caracteres Juan Antonio, veremos que en realidad la cadena leda es Juan, ya que el carcter en blanco acta como delimitador y fuerza el n de la lectura. Si se desea leer una secuencia de caracteres que incluya espacios en blanco, utilizaremos la funcin getline en lugar del operador >>. La funcin getline lee y almacena en una variable de tipo string todos los caracteres del buer de entrada, hasta leer el carcter de n de lnea (ENTER), sin eliminar los espacios iniciales. As pues, en nuestro ejemplo podramos leer nombres compuestos utilizando getline de la siguiente forma:
#include <iostream> #include <string> using namespace std ; int main() { string nombre ; cout << "Introduzca el nombre: " ; getline(cin, nombre) ; cout << "Nombre: " << nombre << endl ; }

Adems, la funcin getline permite especicar el delimitador que marca el nal de la secuencia de caracteres a leer. Si no se especica ninguno (como ocurre en el ejemplo anterior), por defecto se utiliza el carcter de n de lnea. Sin embargo, si se especica el delimitador, lee y almacena todos los caracteres del buer hasta leer el carcter delimitador especicado, el cual es eliminado del buer, pero no es almacenado en la variable. En el siguiente ejemplo se utiliza un punto como delimitador en getline, por lo que la lectura de teclado acaba cuanto se localice dicho carcter.
#include <iostream> #include <string> using namespace std ;
1 Se consideran espacios en blanco los siguientes caracteres: espacio en blanco ( ), tabuladores (\t, \v y \f), retorno de carro (\r) y nueva lnea (\n).

Dpto. Lenguajes y Ciencias de la Computacin

Universidad de Mlaga

58
const char DELIMITADOR = . ; int main() { string nombre ; cout << "Introduzca el nombre: " ; getline(cin, nombre, DELIMITADOR) ; cout << "Nombre: " << nombre << endl ; }

CAPTULO 6. TIPOS COMPUESTOS

Como hemos visto, el comportamiento de las operaciones de lectura con >> y getline es diferente. En ocasiones, cuando se utiliza una lectura con getline despus de una lectura previa con >>, podemos encontrarnos con un comportamiento que, aunque correcto, puede no corresponder al esperado intuitivamente. Conviene que conozcamos con detalle qu ocurre en cada caso, por lo que a continuacin lo veremos sobre un ejemplo. Supongamos que queremos disear un programa que, solicite y muestre el nombre y la edad de cinco personas. Para ello, leeremos el nombre de la persona con getline (ya que puede ser compuesto), y leeremos la edad con el operador de entrada >>, ya que nos permite introducir datos numricos:
#include <iostream> #include <string> using namespace std ; int main() { string nombre ; int edad ; for (int i = 0; i < 5; ++i) { cout << "Introduzca el nombre: " ; getline(cin, nombre) ; cout << "Introduzca edad: " ; cin >> edad ; cout << "Edad: " << edad << " Nombre: [" << nombre << "]" << endl ; } }

Sin embargo, al ejecutar el programa comprobamos que no funciona como esperbamos. La primera iteracin funciona adecuadamente, el ujo de ejecucin espera hasta que se introduce el nombre, y posteriormente espera hasta que se introduce la edad, mostrando dichos datos por pantalla. Sin embargo, las siguientes iteraciones funcionan de forma anmala, ya que la ejecucin del programa no se detiene para que el usuario pueda introducir el nombre. Esto es debido a que no hemos tenido en cuenta cmo se comportan las operaciones de lectura de datos (>> y getline) al obtener los datos del buer de entrada. Hay que considerar que despus de leer la edad en una determinada iteracin, en el buer permanece el carcter de n de lnea (ENTER) que se introdujo tras teclear la edad, ya que ste no es ledo por el operador >>. En la siguiente iteracin, la funcin getline lee una secuencia de caracteres hasta encontrar un ENTER (sin saltar los espacios iniciales), por lo que leer el carcter ENTER que qued en el buer en la lectura previa de la edad de la iteracin anterior, haciendo que nalice la lectura directamente. El resultado es que, al leer el nombre, se lee una cadena vaca, sin necesidad de detener el programa para que el usuario introduzca el nombre de la persona. La solucin a este problema es eliminar los caracteres de espacios en blanco (y n de lnea) del buer de entrada. De esta forma el buer estar realmente vaco y conseguiremos que la ejecucin de getline haga que el programa se detenga hasta que el usuario introduzca el nombre. Hay diferentes formas de conseguir que el buer se quede vaco. Para eliminar los caracteres de espacios en blanco y n de lnea del buer de entrada antes de leer la secuencia de caracteres con getline, utilizaremos el manipulador ws en el ujo cin, que extrae todos los espacios en blanco hasta encontrar algn carcter distinto, por lo que no ser posible leer una cadena de caracteres vaca. Por ejemplo:
Dpto. Lenguajes y Ciencias de la Computacin Universidad de Mlaga

6.2. CADENAS DE CARACTERES EN C++: EL TIPO STRING


#include <iostream> #include <string> using namespace std ; int main() { string nombre ; int edad ; for (int i = 0; i < 5; ++i) { cin >> ws ; // elimina los espacios en blanco y fin de lnea cout << "Introduzca el nombre: " ; getline(cin, nombre) ; cout << "Introduzca edad: " ; cin >> edad ; cout << "Edad: " << edad << " Nombre: [" << nombre << "]" << endl ; } }

59

Tambin es posible que nos interese que la cadena vaca sea una entrada vlida en el programa. En nuestro ejemplo podramos estar interesados en que el usuario introduzca un nombre vaco como respuesta. En este caso, es necesario que el buer se encuentre vaco en el momento de realizar la operacin de entrada. Para ello, eliminaremos los caracteres que pudiera contener el buer (no nicamente espacios en blanco) despus de la ltima operacin de lectura de datos, usando la funcin ignore. Por ejemplo:
#include <iostream> #include <string> using namespace std ; int main() { string nombre ; int edad ; for (int i = 0; i < 5; ++i) { cout << "Introduzca el nombre: " ; getline(cin, nombre) ; cout << "Introduzca edad: " ; cin >> edad ; cin.ignore(10000, \n) ; // elimina todos los caracteres del buffer hasta \n cout << "Edad: " << edad << " Nombre: [" << nombre << "]" << endl ; } }

La funcin ignore elimina todos los caracteres del buer de entrada en el ujo especicado, hasta que se hayan eliminado el nmero de caracteres indicado en el primer argumento o bien se haya eliminado el carcter indicado en el segundo. Ntese que la sentencia cin >> ws se asocia a la funcin getline que le sigue, mientras que la sentencia ignore se asocia a la sentencia de entrada >> que le precede.

6.2.2.

Operadores predenidos

A continuacin, usaremos algunos ejemplos para presentar los operadores predenidos ms habituales sobre cadenas de tipo string. Asignacin (=) de valores de tipo string a variables de tipo string:
#include <iostream> #include <string> using namespace std ; const string AUTOR = "Jos Luis" ; int main()
Dpto. Lenguajes y Ciencias de la Computacin Universidad de Mlaga

60
{ string nombre = "Pepe" ; // ... nombre = AUTOR ; }

CAPTULO 6. TIPOS COMPUESTOS

Comparaciones lexicogrcas2 (==, !=, >, >=, <, <=): if (nombre >= AUTOR) { /*...*/ } Concatenacin de cadenas y de caracteres (+, +=):
#include <iostream> #include <string> using namespace std ; const string AUTOR = "Jos Luis" ; int main () { string nombre = AUTOR + "Lpez" ; nombre += "Vzque" ; nombre += z ; nombre = AUTOR + s ; }

Obtencin del nmero de caracteres que componen la cadena (size): unsigned ncar = nombre.size(); if (nombre.size() == 0) { /*...*/ } Acceso al i-simo carcter de la cadena ([]). El carcter accedido es de tipo char: char c = nombre[i]; donde i [0..nombre.size()-1] nombre[i] = z; donde i [0..nombre.size()-1] Es importante tener en cuenta que el ndice utilizado para acceder al carcter de la cadena debe corresponder a una posicin vlida de la misma. El acceso fuera del rango vlido (por ejemplo, para aadir caracteres al nal) es un error que hay que evitar. Aunque se trata de un error del programa, durante su ejecucin no se nos avisa del mismo3 . A partir de dicho momento el comportamiento del programa quedara indeterminado. El tipo string dispone del operador at para acceder a posiciones de la cadena controlando posibles errores de acceso, pero no ser utilizado es este curso. Obtencin de una nueva subcadena (de tipo string) a partir del ndice i. Se puede especicar el tamao de la subcadena. Si no se especica, o si el tamao (sz) especicado excede al nmero de caracteres que hay desde i, entonces se toma la subcadena desde el ndice hasta el nal: string sb = nombre.substr(i); donde i [0..nombre.size()-1] string sb = nombre.substr(i, sz); donde i [0..nombre.size()-1] Ntese que no es vlida la asignacin a una subcadena: nombre.substr(i, sz) = "...";

6.2.3.

Ejemplos

A continuacin, mostramos algunos ejemplos de programas en los que trabajamos con cadenas de caracteres representadas mediante el tipo string.
comparacin lexicogrca se basa en la ordenacin alfabtica, comnmente utilizada en los diccionarios. GNU C++ la opcin de compilacin -D_GLIBCXX_DEBUG permite comprobar los ndices de acceso de forma automtica.
3 En 2 La

Dpto. Lenguajes y Ciencias de la Computacin

Universidad de Mlaga

6.2. CADENAS DE CARACTERES EN C++: EL TIPO STRING Ejemplo 1. Conversin de una palabra a maysculas

61

Comenzamos con un programa que lee una palabra por teclado, la convierte a maysculas y la muestra en pantalla. Ntese que, aunque este programa es simple, no ha sido resuelto directamente en la funcin main. La lectura utiliza el operador >>, por lo que el programa nicamente puede recibir palabras que no incluyan espacios. El procedimiento mayusculas utiliza un parmetro de entrada/salida, por lo que se usa paso por referencia, como es habitual en cualquier tipo. La manipulacin de la cadena, carcter a carcter, se realiza mediante un bucle tpico en el que usamos size para conocer el nmero de caracteres de la cadena y el operador [] para acceder a un carcter concreto de la misma.
#include <iostream> #include <string> using namespace std ; // -- Subalgoritmos ---void mayuscula (char& letra) { if ((letra >= a) && (letra <= z)) { letra = letra - a + A ; } } void mayusculas (string& palabra) { for (unsigned i = 0 ; i < palabra.size() ; ++i) { mayuscula(palabra[i]) ; } } // -- Principal -------int main () { string palabra ; cin >> palabra ; mayusculas(palabra) ; cout << palabra << endl ; }

Ntese que un carcter concreto de la cadena es de tipo char, y se pasa por referencia a un subprograma que convierte una letra a su equivalente en mayscula. Ejemplo 2. Plural de una palabra A continuacin mostramos un programa que lee por teclado una palabra en minsculas y muestra en pantalla su plural. El programa est diseado en base a un procedimiento que usa un parmetro de entrada/salida en el que recibe la palabra original y devuelve su plural. Las reglas para convertir una palabra en plural son las siguientes: Si acaba en vocal se le aade la letra s. Si acaba en consonante se le aaden las letras es. Si la consonante es la letra z, se sustituye por la letra c. Suponemos que la palabra introducida es correcta y est formada por letras minsculas. Necesitamos acceder a la ltima letra de la cadena para determinar en qu caso nos encontramos, por lo que usamos size y el operador de acceso para consultar cul es el ltimo carcter. Una vez seleccionado el caso adecuado, procedemos a aadir caracteres a la palabra segn corresponda. Usamos el procedimiento plural_1 en el que se utiliza el operador de concatenacin para aadir la terminacin adecuada a la cadena. En caso de ser necesario, se accede a la ltima letra para cambiar la z por c. Ntese que para ello usamos el operador de acceso. Ello es posible porque
Dpto. Lenguajes y Ciencias de la Computacin Universidad de Mlaga

62

CAPTULO 6. TIPOS COMPUESTOS

dicha letra pertenece a la cadena y lo que queremos es sustituir un carcter existente por otro. Sin embargo, no es posible utilizar el operador de acceso para aadir la terminacin s al nal de la cadena, porque intentaramos acceder a un carcter no existente en la misma, generando con ello un error. Aunque desde el programa principal se hace uso del procedimiento plural_1, tambin se muestra una implementacin alternativa en el procedimiento plural_2. En este caso nos basamos en la posibilidad de utilizar substr para obtener una subcadena. Cuando es necesario, tomamos la cadena excluyendo la letra nal y al resultado le concatenamos la terminacin "ces".
#include <iostream> #include <string> using namespace std ; // -- Subalgoritmos ---bool es_vocal (char c) { return (c == a) || (c == e) || (c == i) || (c == o) || (c == u) ; } void plural_1 (string& palabra) { if (palabra.size() > 0) { if (es_vocal(palabra[palabra.size() - 1])) { palabra += s ; } else { if (palabra[palabra.size() - 1] == z) { palabra[palabra.size() - 1] = c ; } palabra += "es" ; } } } void plural_2 (string& palabra) { if (palabra.size() > 0) { if (es_vocal(palabra[palabra.size() - 1])) { palabra += s ; } else if (palabra[palabra.size() - 1] == z) { palabra = palabra.substr(0, palabra.size() - 1) + "ces" ; } else { palabra += "es" ; } } } // -- Principal -------int main () { string palabra ; cin >> palabra ; plural_1(palabra) ; cout << palabra << endl ; }

Ejemplo 3. Funcin palndromo Se dice que una cadena es un palndromo si se puede leer igual de derecha a izquierda o de izquierda a derecha, por ejemplo, ana. Queremos implementar una funcin que reciba como parmetro de entrada una cadena con una palabra y devuelva si se trata de un palndromo. Como el parmetro de entrada es de tipo string (un tipo compuesto), utilizamos paso por referencia constante. La implementacin se basa en considerar que, para que la cadena sea palndromo, deben
Dpto. Lenguajes y Ciencias de la Computacin Universidad de Mlaga

6.3. REGISTROS O ESTRUCTURAS

63

coincidir dos a dos cada par de letras situadas en posiciones simtricas respecto al carcter situado en la posicin central de la cadena. Usamos un bucle para comparar cada par de letras y dos ndices para hacer referencia, respectivamente, al carcter a la izquierda y a la derecha de la cadena. Si el bucle termina despus de comparar todos los posibles pares, entonces se trata de un palndromo.
bool es_palindromo (const string& palabra) { bool ok = false ; if (palabra.size() > 0) { unsigned i = 0 ; unsigned j = palabra.size() - 1 ; while ((i < j) && (palabra[i] == palabra[j])) { ++i ; --j ; } ok = i >= j ; } return ok ; }

Ejemplo 4. Sustituir una subcadena por otra Queremos disponer de un subprograma que reciba una cadena y reemplace la subcadena que empieza en una cierta posicin i y con un tamao sz por una nueva cadena nueva. Por ejemplo, al reemplazar en la cadena "camarero" la subcadena que comienza en la posicin 2 y tiene 3 caracteres por la cadena sill, la cadena se convierte en "casillero". El siguiente subprograma reemplazar dene el parmetro de entrada/salida str (por referencia), los parmetros de entrada i y sz (por valor, al ser de tipos simples) y el parmetro de entrada nueva (por referencia constante, al ser de tipo string). Su implementacin se basa en usar subsr para obtener las subcadenas adecuadas y el operador de concatenacin para formar la cadena resultante adecuadamente.
void reemplazar (string& str, unsigned i, unsigned sz, const string& nueva) { if (i + sz < str.size()) { str = str.substr(0, i) + nueva + str.substr(i + sz, str.size() - (i + sz)) ; } else if (i <= str.size()) { str = str.substr(0, i) + nueva ; } }

Nota: La biblioteca <string> contiene la funcin replace, que podra haber sido utilizada directamente para obtener el objetivo propuesto. Este subprograma es equivalente a la operacin str.replace(i, sz, nueva).

6.3.

Registros o Estructuras

Un registro representa un valor compuesto por un nmero determinado de elementos, que pueden ser de distintos tipos (simples y compuestos). Para utilizar registros deniremos un nuevo tipo registro , enumerando los elementos (campos ) que lo componen. Para cada campo deberemos indicar su tipo y el identicador con el nos referiremos al mismo. La denicin del tipo registro se har utilizando la palabra reservada struct, seguido del identicador con el que haremos referencia a dicho tipo. A continuacin se enumeran, entre llaves, los campos que lo componen, especicando su tipo y el identicador con el que referenciarlo, seguido por el delimitador punto y coma (;). La llave de cierre debe ir seguida de punto y coma. A continuacin mostramos la denicin de un tipo para representar fechas, dadas por tres nmeros que representan un cierto da de un mes de un ao.
Dpto. Lenguajes y Ciencias de la Computacin Universidad de Mlaga

64
struct Fecha unsigned unsigned unsigned } ; { dia ; mes ; anyo ;

CAPTULO 6. TIPOS COMPUESTOS

Una vez denido el tipo registro, podr ser utilizado como cualquier otro tipo, para denir constantes simblicas, variables o parmetros formales en los subprogramas. Por ejemplo, podemos utilizar el nuevo tipo Fecha para denir variables:
Fecha f_ingreso ;

o para denir constantes:


const Fecha F_NAC = { 20, 2, 2001} ;

Los valores de tipo estructurado estn formados por diferentes componentes, por lo que necesitamos alguna notacin especial para indicar claramente el valor de cada una de ellos. Como se puede observar en el ejemplo, el valor que queremos que tome la constante F_NAC se expresa enumerando y separando por comas los valores que queremos asignar a los campos (en el mismo orden de la denicin del tipo registro) y utilizando llaves para agruparlo todo. Los valores del tipo Fecha se componen de tres elementos concretos, el da (de tipo unsigned), el mes (de tipo unsigned) y el ao (de tipo unsigned). Los identicadores dia, mes y anyo representan los nombres de sus elementos componentes, denominados campos, cuyo mbito de visibilidad se restringe a la propia denicin del registro. Los campos de un registro pueden ser de cualquier tipo de datos, simple o compuesto. Por ejemplo, podramos estar interesados en tratar con informacin de empleados, deniendo un nuevo tipo Empleado como un registro que contiene el nombre del empleado (de tipo string), su cdigo y sueldo (de tipo unsigned) y su fecha de ingreso en la empresa (de tipo Fecha).
// -- Tipos -----------struct Empleado { string nombre ; unsigned codigo ; unsigned sueldo ; Fecha fecha_ingreso ; } ; // -- Principal -------int main () { Empleado e ; // ... }

Una vez declarada una entidad (constante o variable) de tipo registro, por ejemplo la variable f_ingreso, podemos referirnos a ella en su globalidad (realizando asignaciones y pasos de parmetros) o acceder a sus componentes (campos) utilizando el operador punto (.). Una vez que accedemos a un determinado campo tenemos un valor del tipo de dicho campo, por lo que podr ser utilizado de acuerdo a las caractersticas de dicho tipo, como si se tratase de una variable de dicho tipo.
int main () { Fecha f_nac, hoy ; hoy.dia = 18 ; hoy.mes = 10 ; hoy.anyo = 2001 ; f_nac = hoy ; }
Dpto. Lenguajes y Ciencias de la Computacin Universidad de Mlaga

dia: mes: anyo:

18 10 2001 f_nac

dia: mes: anyo:

18 10 2001 hoy

6.3. REGISTROS O ESTRUCTURAS

65

6.3.1.

Operaciones con registros completos

Adems de utilizar el operador punto para acceder individualmente a campos concretos de un registro, es posible efectuar operaciones con un registro completo, sin considerar campos concretos. nicamente se permiten tres tipos de operaciones con registros completos: Asignacin. Es posible utilizar el operador de asignacin para asignar un valor de tipo registro a una variable del mismo tipo registro. Por ejemplo, si f1 y f2 son dos variables de tipo Fecha, podramos hacer lo siguiente para almacenar cada uno de los campos de f2 en el correspondiente campo de f1.
f1 = f2 ;

cuyo efecto es equivalente a la copia uno a uno de los campos, aunque obviamente expresado de forma ms legible, intuitiva y menos propensa a errores.
f1.dia = f2.dia ; f1.mes = f2.mes ; f1.anyo = f2.anyo ;

En general, es posible asignar un valor de tipo registro completo a una variable o campo, siempre que sea del mismo tipo. Por ejemplo, podramos asignar un registro de tipo Fecha al campo fecha_ingreso de un registro de tipo Empleado, ya que tanto el valor que se asigna como el elemento al que se asigna son del mismo tipo.
Empleado e; Fecha f2 = { 18, 10, 2001 } ; e.nombre = "Juan" ; e.codigo = 101 ; e.sueldo = 1000 ; e.fecha_ingreso = f2;

No hay ninguna otra operacin disponible con registros completos. Sin embargo, esto no constituye ninguna limitacin, porque el programador puede denir subprogramas que reciban registros como parmetros. De esta forma puede disponer de operaciones que hagan ms simple el diseo del programa4 .

6.3.2.

Entrada/Salida de valores de tipo registro

Un tipo registro es denido por el programador, por lo que no existe un mecanismo predenido para la lectura o escritura de valores de tipo registro. El programador deber ocuparse de la lectura/escritura de un registro, efectuando la lectura/escritura de cada uno de sus campos. Es recomendable ocultar todos estos detalles por lo que, en general, deniremos subprogramas para leer o escribir valores de cada uno de los tipos registros que se utilicen en un programa. Por ejemplo, en un programa que utilizara el tipo Fecha, sera recomendable denir los siguientes subprogramas:
void leer_fecha (Fecha& f) { cin >> f.dia >> f.mes >> f.anyo ; } void escribir_fecha (const Fecha& f) { cout << f.dia << "/" << f.mes << "/" << f.anyo ; }
4 Ntese

que los operadores tpicos de comparacin (==, !=, >, <, . . . ) no son vlidos entre valores de tipo registro. Universidad de Mlaga

Dpto. Lenguajes y Ciencias de la Computacin

66

CAPTULO 6. TIPOS COMPUESTOS

6.3.3.

Ejemplo. Uso de registros

A continuacin presentamos un programa en el que trabajamos con informacin que representa instantes de tiempo, dados por tres nmeros que representan cierta hora, minuto y segundo. El programa lee de teclado dos valores de tiempo y muestra en pantalla la diferencia entre ambos. Podramos abordar este programa sin necesidad de denir tipos registros, utilizando mltiples variables para representar cada uno de los elementos manipulados. Por ejemplo, podramos denir las variables d1, h1 y m1 para representar la hora, minuto y segundo del primer instante. Procederamos igual con el segundo, con la diferencia, etc. El resultado sera un programa con multitud de variables con las que hacer referencia a conceptos que, en realidad, estn relacionados. Por este motivo, resulta ms adecuado denir un nuevo tipo Tiempo como un registro con tres campos, representando la hora, los minutos y los segundos. De esta forma, para representar un instante dado bastar con un nico valor de tipo Tiempo, compuesto de tres campos. Manejamos la misma informacin, pero de una forma ms organizada, legible y compacta. El siguiente programa muestra esta segunda solucin. Como se puede ver, la denicin del tipo Tiempo nos permite usar nicamente tres variables en el programa principal, que corresponden con los conceptos manejados en el mismo: el primer tiempo, el segundo y su diferencia. As mismo, la descomposicin modular del programa, deniendo subprogramas para leer, escribir convertir valores de tiempo a segundos o calcular la diferencia de dos instantes de tiempo, permite que el programa principal quede muy legible y resulte intuitivo.
#include <iostream> #include <string> using namespace std ; // -- Constantes ------const unsigned SEGMIN = 60 ; const unsigned MINHOR = 60 ; const unsigned MAXHOR = 24 ; const unsigned SEGHOR = SEGMIN * MINHOR ; // -- Tipos -----------struct Tiempo { unsigned horas ; unsigned minutos ; unsigned segundos ; } ; // -- Subalgoritmos ---unsigned leer_rango (unsigned inf, unsigned sup) { unsigned num ; do { cin >> num ; } while ( ! ((num >= inf) && (num < sup))) ; return num ; } void leer_tiempo (Tiempo& t) { t.horas = leer_rango(0, MAXHOR) ; t.minutos = leer_rango(0, MINHOR) ; t.segundos = leer_rango(0, SEGMIN) ; } void escribir_tiempo (const Tiempo& t) { cout << t.horas << ":" << t.minutos << ":" << t.segundos ; } unsigned tiempo_a_seg (const Tiempo& t) { return (t.horas * SEGHOR) + (t.minutos * SEGMIN) + (t.segundos) ; }
Dpto. Lenguajes y Ciencias de la Computacin Universidad de Mlaga

6.4. AGREGADOS: EL TIPO ARRAY


void seg_a_tiempo (unsigned sg, Tiempo& t) { t.horas = sg / SEGHOR ; t.minutos = (sg % SEGHOR) / SEGMIN ; t.segundos = (sg % SEGHOR) % SEGMIN ; } void diferencia (const Tiempo& t1, const Tiempo& t2, Tiempo& dif) { seg_a_tiempo(tiempo_a_seg(t2) - tiempo_a_seg(t1), dif) ; } // -- Principal -------int main () { Tiempo t1, t2, dif ; leer_tiempo(t1) ; leer_tiempo(t2) ; diferencia(t1, t2, dif) ; escribir_tiempo(dif) ; cout << endl ; }

67

6.4.

Agregados: el Tipo Array

Un array representa un valor compuesto por un nmero determinado (denido en tiempo de compilacin) de elementos de un mismo tipo de datos. Este tipo de valores son tiles en aquellas situaciones en las que necesitamos almacenar y manipular una coleccin de valores y acceder a ellos de forma parametrizada, normalmente para aplicar un proceso iterativo. Por ejemplo, podramos estar interesados en almacenar notas de un examen de los 100 alumnos de un curso y, posteriormente, calcular la media de aquellos que aprueban. El programador denir un nuevo tipo agregado cuando decida que necesita manipular valores con las caractersticas mencionadas. Para ello, nos basaremos en el tipo array de la biblioteca estndar de C++5 , por lo que habr que incluir la biblioteca <array>. Un tipo agregado se dene utilizando typedef (indicando as que estamos deniendo un nuevo tipo de datos), seguido de una descripcin de las caractersticas de los elementos de dicho tipo y de un nombre con el que identicarlo. Las caractersticas del nuevo tipo se describen utilizando array e indicando el tipo de sus elementos (su tipo base ) y el nmero de elementos, que debe ser una constante conocida en tiempo de compilacin. El resultado es un array con el nmero de elementos y el tipo especicado, en el que cada elemento est identicado por un valor numrico que va desde cero (primer elemento) hasta el nmero de elementos menos uno (ltimo elemento). Por ejemplo, podemos denir un nuevo tipo Vector como un agregado de 5 elementos, cada uno del tipo int:
#include <array> using namespace std ; // -- Constantes ------const int NELMS = 5 ; // -- Tipos -----------typedef array<int, NELMS> Vector ;

Posteriormente podremos usar dicho tipo Vector para denir variables y constantes como es usual. Sin embargo, como ahora tratamos con valores compuestos, las constantes literales del tipo array se especican entre llaves dobles. Por ejemplo, a continuacin denimos una constante PRIMOS con los primeros nmeros primos, y una variable v, cuyo valor inicial est sin especicar.
5 El tipo array de la biblioteca estndar est disponible desde el estndar C++11. Existen otras formas de trabajar con arrays, aunque no las estudiaremos en este curso.

Dpto. Lenguajes y Ciencias de la Computacin

Universidad de Mlaga

68

CAPTULO 6. TIPOS COMPUESTOS

// -- Constantes ------const Vector PRIMOS = {{ 2, 3, 5, 7, 11 }} ; // -- Principal -------int main () { Vector v; v: ? ? ? ? ? } 0 1 2 3 4

PRIMOS:

2
0

3
1

5
2

7
3

11
4

El tipo base (de los elementos) del array puede ser simple o compuesto. Por ejemplo, podemos denir un nuevo tipo Citas como un agregado de 4 elementos, cada uno del tipo Fecha, y denir variables y constantes de dicho tipo:
struct Fecha { unsigned dia; unsigned mes; unsigned anyo; }; const int N_CITAS = 4; typedef array<Fecha, N_CITAS> Citas ; const Citas CUMPLEANYOS = {{ { 1, 1, 2001 }, { 2, 2, 2002 }, { 3, 3, 2003 }, { 4, 4, 2004 } }} ; int main() { Citas cit; }

CUMPLEANYOS: 1 1 2001
0

2 2 2002
1

3 3 2003
2

4 4 2004
3

Al igual que cuando trabajamos con registros nos interesa acceder a sus campos para manipular sus valores adecuadamente, al trabajar con arrays nos interesar acceder a sus componentes individuales. Para ello, en este caso se utiliza el operador ([]), indicando dentro de los corchetes el ndice de la posicin que ocupa el elemento al que nos referimos. Por ejemplo, con cit[0] accedemos a un componente de tipo Fecha situado en la primera posicin del array cit. Una vez que accedemos a un elemento del array, ste puede ser utilizado exactamente igual que un valor del tipo base del mismo. En nuestro ejemplo, una vez que accedemos a cit[0], lo que tenemos es un registro de tipo Fecha, por lo que podremos manipularlo exactamente igual que si se tratara de una variable de dicho tipo. As, si quisiera establecer que el da almacenado en la fecha situada en el primera componente del array sea el 22, bastara con hacer lo siguiente:
cit[0].dia = 22 ;

El programador es responsable de hacer un uso adecuado de los elementos del array, accediendo a posiciones vlidas del mismo. Para ello, deber tener en cuenta que el ndice del primer elemento del array es 0 y el ndice del ltimo elemento viene dado por el nmero de elementos con que se ha denido menos uno. Dicho nmero de elementos es conocido por ser el valor de la constante utilizada en el typedef (en nuestro ejemplo N_CITAS) , aunque resulta ms adecuado utilizarlo accediendo a la funcin size() sobre la variable de tipo array correspondiente (en nuestro ejemplo, cit.size()). Si por error se intentara acceder a una posicin no vlida de un array, se estara generando una situacin anmala. No se producira ningn aviso de dicho error y, a partir de ese momento, el programa podra tener un comportamiento inesperado. Por ejemplo, el siguiente programa dene una variable de tipo Citas en la que almacena unas determinadas fechas. Sin embargo, al salir del bucle accede a una posicin errnea.
struct Fecha { unsigned dia, mes, anyo;
Dpto. Lenguajes y Ciencias de la Computacin Universidad de Mlaga

6.4. AGREGADOS: EL TIPO ARRAY


}; const int N_CITAS = 4; typedef array<Fecha, N_CITAS> Citas ; int main() { Citas cit; cit[0].dia = 18; cit[0].mes = 10; cit[0].anyo = 2001; for (int i = 0; i < cit.size(); ++i) { cit[i].dia = 1; cit[i].mes = 1; cit[i].anyo = 2002; } cit[N_CITAS] = { 1, 1, 2002 }; // ERROR. Acceso fuera de los limites // ... }

69

Si utilizamos el compilador GNU C++, es posible compilar con la opcin de compilacin -D_GLIBCXX_DEBUG, que permite comprobar los ndices al acceder a un array6 Tambin es posible acceder a un determinado elemento del array mediante la operacin at(), la cual controla posibles errores de acceso, pero no ser utilizada en este curso.

6.4.1.

Operadores predenidos

Los tipos arrays denidos en base al tipo array de la biblioteca estndar de C++ se pueden manipular como el resto de valores de otros tipos de datos, por lo que se dispone de forma predenida de los siguientes operadores: Asignacin (=) de valores de un tipo array a una variable del mismo tipo array. Por ejemplo, dado el tipo Vector introducido anteriormente, podramos hacer:
Vector v1, v2; ... v1 = v2;

Comparacin de igualdad (==). Se obtiene true o false segn coincida o no cada elemento del primer array con el elemento correspondiente (en la misma posicin) del segundo array. Es aplicable si el operador == est denido para elementos del tipo base. Comparacin de desigualdad (!=). Se obtiene true si algn elemento del primer array no coincide con el elemento correspondiente (en la misma posicin) del segundo. Es aplicable si el operador != est denido para elementos del tipo base. Comparaciones lexicogrcas (>, <, >=, <=). Se obtiene true si el primer operando satisface la operacin especicada respecto al segundo. Es aplicable si el operador relacional est denido para elementos del tipo base. Paso como parmetro a subprogramas. Al igual que ocurre con los valores de otros tipos compuestos, podremos pasar arrays como parmetros a subprogramas. Si el parmetro es de salida de entrada/salida usaremos paso por referencia, y si es de entrada usaremos paso por referencia constante. Por ejemplo, si quisiramos leer y escribir valores de tipo Citas podramos declarar los subprogramas:
void leer_citas (Citas& c); void escribir_citas (const Citas& c);
6 Si

// parametro de salida // parametro de entrada

el alumno desea utilizar esta opcin, debe descargar la biblioteca de la pgina web de la asignatura Universidad de Mlaga

Dpto. Lenguajes y Ciencias de la Computacin

70

CAPTULO 6. TIPOS COMPUESTOS Entrada/salida. Al igual que ocurre en el caso de los tipos registro y, en general, para cualquier tipo de datos denido por el usuario, no es posible disponer de operaciones predenidas en C++ para su entrada/salida. El programador deber ocuparse de efectuar la entrada/salida de datos de tipo array, leyendo o escribiendo cada componente segn el tipo de que se trate.

6.4.2.

Ejemplos

A continuacin, mostraremos la utilidad prctica de la denicin de tipos arrays, mediante ejemplos cuya solucin sera poco factible si nicamente dispusiramos de los tipos vistos hasta ahora. Ejemplo 1. Vectores Supongamos que queremos disear un programa que trabaje con vectores de 5 elementos. Sabemos que utilizaremos algunas operaciones tpicas sobre los vectores como leer todas las componentes desde teclado, imprimir el vector, calcular el producto escalar de dos vectores, calcular su suma, etc. Podramos haber pensado en declarar 5 variables individuales para representar las componentes de cada vector, (por ejemplo, v11, v12, v13, v14 y v15), pero resulta evidente que este enfoque nos conduce a una solucin inaceptable. Nos llevara a un programa con multitud de variables y casi imposible de manejar Qu hacemos si el vector tiene 100 elementos?, cmo abordamos posibles cambios futuros?. En este ejemplo la nica solucin factible es denir un nuevo tipo que est compuesto por 5 elementos del mismo tipo base y sobre el que podamos iterar para acceder sucesivamente a sus diferentes componentes. A continuacin denimos el tipo Vector y procesamos sus elementos mediante bucles. Ello nos permite recorrer los elementos, visitando uno a uno cada elemento, para efectuar la operacin adecuada en cada caso. Por ejemplo, la lectura del vector se basa en un bucle en el que usamos una variable de control y hacemos que tome el valor que nos interesa para determinar la posicin del elemento del array en la que almacenar el valor ledo.
#include <iostream> #include <array> using namespace std; // -- Constantes ------const unsigned NELMS = 5; // -- Tipos -----------typedef array<int, NELMS> Vector; // -- Subalgoritmos ---void leer (Vector& v) { for (unsigned i = 0; i < v.size(); ++i) { cin >> v[i]; } } int sumar (const Vector& v) { int suma = 0; for (unsigned i = 0; i < v.size(); ++i) { suma += v[i]; } return suma; } // -- Principal -------int main () { Vector v1, v2; leer(v1); leer(v2);
Dpto. Lenguajes y Ciencias de la Computacin Universidad de Mlaga

6.4. AGREGADOS: EL TIPO ARRAY


if (sumar(v1) == sumar(v2)) { cout << "Misma suma" << endl; } }

71

Ejemplo 2. Clculo del sueldo de los agentes en una empresa Queremos implementar un programa que calcule e imprima en pantalla el sueldo de los 20 agentes de ventas de una empresa. Cada agente cobra un sueldo jo de 1000 Cms un incentivo, que ser un 10 % de las ventas que ha realizado. Sin embargo, dicho incentivo no ser percibido por todos los agentes, sino slo por aquellos cuyas ventas superen los 2/3 de la media de ventas del total de los agentes. El planteamiento del problema nos obliga a almacenar las ventas de todos los agentes, porque no es posible decidir si un agente cobra o no el incentivo hasta que se hayan ledo las ventas de todos los agentes y calculado el umbral que determina si un agente cobra o no su incentivo. De nuevo, debemos excluir la posibilidad de usar variables individuales para las ventas de cada agente, y optaremos por organizar las ventas de todos los agentes deniendo un nuevo tipo compuesto en el que cada componente represente las ventas de un determinado agente. En el siguiente programa denimos el tipo Ventas y los subprogramas adecuados para procesar valores de dicho tipo. Usamos el esquema tpico para el paso de parmetros, segn se trate de parmetros de entrada o de salida y utilizamos los bucles adecuados para recorrer cada array, accediendo y procesando sus componentes una a una.
#include <iostream> #include <array> using namespace std; // -- Constantes ------const unsigned NAGENTES = 20; const double SUELDO_FIJO = 1000.0; const double INCENTIVO = 10.0; const double PROMEDIO = 2.0 / 3.0; // -- Tipos -----------typedef array<double, NAGENTES> Ventas; // -- Subalgoritmos ---double calc_media (const Ventas& v) { double suma = 0.0; for (unsigned i = 0; i < v.size(); ++i) { suma += v[i]; } return suma / double(v.size()); } double porcentaje (double p, double valor) { return (p * valor) / 100.0; } void leer_ventas (Ventas& v) { for (unsigned i = 0; i < v.size(); ++i) { cout << "Introduzca ventas del Agente " << i << ": "; cin >> v[i]; } } void imprimir_sueldos (const Ventas& v) { double umbral = PROMEDIO * calc_media(v); for (unsigned i = 0; i < v.size(); ++i) { double sueldo = SUELDO_FIJO;
Dpto. Lenguajes y Ciencias de la Computacin Universidad de Mlaga

72

CAPTULO 6. TIPOS COMPUESTOS


if (v[i] >= umbral) { sueldo += porcentaje(INCENTIVO, v[i]); } cout << "Agente: " << i << " Sueldo: " << sueldo << endl; } } // -- Principal -------int main () { Ventas ventas; leer_ventas(ventas); imprimir_sueldos(ventas); }

6.4.3.

Agregados Incompletos

Hay situaciones en las que no se conoce exactamente la cantidad de elementos que deseamos almacenar en un array. Por otra parte, sabemos que el tipo array requiere que el nmero de elementos del mismo sea jo y conocido en tiempo de compilacin (antes de ejecutar el programa). Como consecuencia, no es posible hacer que el tamao del array se adapte exactamente al tamao necesitado durante la ejecucin. En estos casos optaremos por seleccionar un tipo array con un nmero de elementos que permita almacenar el mayor nmero de elementos previsto en el programa. En estas situaciones nos encontramos con que el tamao del array no coincide con el nmero de elementos almacenados en el mismo. Pensemos, por ejemplo, en un programa que gestione una agenda de contactos como el que presentamos en la seccin 6.5. En este caso, al principio no hay ningn contacto almacenado y, a medida que se aaden o eliminan contactos, su nmero va variando. La agenda se dene como un array con un nmero jo de elementos, pero al principio del programa no hay ninguno y durante su ejecucin vamos aadiendo y eliminando contactos segn decida el usuario. Necesitamos alguna forma de determinar claramente qu elementos del array contienen los datos reales del programa. Por ejemplo, si en nuestra agenda tenemos tres contactos, cuales son?: los tres primeros?, los tres ltimos?, el primero y los dos ltimos?. La solucin a esta situacin es programar una estrategia para almacenar los elementos en el array y, posteriormente, usar la misma estrategia para localizarlos. Se pueden seguir dos enfoques: almacenar los elementos contiguos en el array o bien permitir que estn dispersos, con posibles huecos entre ellos. Por lo general, gestionar el array con huecos durante la ejecucin del programa suele ser complejo e ineciente. As pues, salvo que se justique lo contrario, optaremos por mantener los elementos vlidos del array almacenados en posiciones consecutivas del mismo. En este caso, necesitamos algn criterio para determinar en qu posicin del array acaban los elementos vlidos y comienzan las posiciones que no nos interesan. Hay dos posibilidades: Denir un elemento reconocible del array que nos permita localizar el punto de frontera entre ambas zonas. Contabilizar el nmero de elementos vlidos almacenados en el array. La primera opcin suele requerir la localizacin del elemento que delimita la frontera entre los elementos vlidos y los no utilizados. Por este motivo, esta opcin suele ser, en la mayora de los casos, ms compleja e ineciente. Nosotros optaremos por seguir la segunda alternativa. En este caso, deberemos plantearnos cmo conocer el nmero de elementos vlidos del array. De nuevo, ahora se plantean dos opciones: mantener dicho nmero independientemente del array (en una variable adicional), o bien asociarlo al array al que se reere, deniendo un registro que contenga dos campos: el array con los elementos almacenados y el nmero de elementos vlidos del mismo. En general, optaremos por esta segunda posibilidad, ya que ello da lugar a programas con mejores caractersticas y no introduce complejidad adicional. Este enfoque nicamente requiere
Dpto. Lenguajes y Ciencias de la Computacin Universidad de Mlaga

6.4. AGREGADOS: EL TIPO ARRAY

73

efectuar la correspondiente denicin de tipos en base a un registro y el acceso a los elementos del array y al nmero total de elementos almacenados en el mismo, sabiendo que se trata de campos de un determinado valor de tipo registro. Por ejemplo, en el programa de gestin de sueldos de los agentes de ventas, podramos considerar un nmero variable de agentes (con un mximo de 20). De esta forma, ahora deniramos el tipo Ventas de la siguiente forma:
#include <iostream> #include <array> using namespace std; // -- Constantes ------const unsigned MAX_AGENTES = 20; const double SUELDO_FIJO = 1000.0; const double INCENTIVO = 10.0; const double PROMEDIO = 2.0 / 3.0; // -- Tipos -----------typedef array<double, MAX_AGENTES> Datos; struct Ventas { unsigned nelms; Datos elm; };

Como se puede observar, los subprogramas calc_media e imprimir_sueldos son prcticamente idnticos a los presentados en la versin anterior salvo que, como el array es un campo de un registro, ahora usamos una notacin diferente para acceder a sus componentes. De forma similar, ahora el nmero de agentes no es un valor jo, sabemos que est almacenado en el campo correspondiente del mismo registro.
// -- Subalgoritmos ---double calc_media (const Ventas& v) { double suma = 0.0; for (unsigned i = 0; i < v.nelms; ++i) { suma += v.elm[i]; } return suma / double(v.nelms); } double porcentaje (double p, double valor) { return (p * valor) / 100.0; } void imprimir_sueldos (const Ventas& v) { double umbral = PROMEDIO * calc_media(v); for (unsigned i = 0; i < v.nelms; ++i) { double sueldo = SUELDO_FIJO; if (v.elm[i] >= umbral) { sueldo += porcentaje(INCENTIVO, v.elm[i]); } cout << "Agente: " << i << " Sueldo: " << sueldo << endl; } }

En este problema debemos optar por un criterio para leer los elementos de la entrada. En la versin anterior bastaba con leer 20 nmeros, porque sabamos que siempre haba 20 agentes. Ahora el nmero de elementos a introducir puede ser diferente y deberemos decidir cmo queremos que tenga lugar la lectura de datos. En el programa mostramos dos de las opciones ms frecuentes para este tipo de casos: Que el usuario introduzca datos hasta teclear un valor que indique el n del proceso de lectura. En nuestro caso, (en el subprograma leer_ventas_1) detectamos el n del proceso
Dpto. Lenguajes y Ciencias de la Computacin Universidad de Mlaga

74

CAPTULO 6. TIPOS COMPUESTOS de lectura cuando, o bien se han introducido las ventas del nmero mximo de agentes, o bien se introduce un dato de ventas incorrecto (es cero o menor).
// ----------------------------------void leer_ventas_1 (Ventas& v) { double vent_ag; v.nelms = 0; cout << "Introduzca ventas del agente " << v.nelms + 1 << ": "; cin >> vent_ag; while ((v.nelms < v.size())&&(vent_ag > 0)) { v.elm[v.nelms] = vent_ag; ++v.nelms; cout << "Introduzca ventas del agente " << v.nelms + 1 << ": "; cin >> vent_ag; } }

Que el usuario comunique por adelantado el nmero de datos de agentes a leer en total. Esta opcin resulta ms simple, porque se puede programar con el mismo esquema usado para un nmero jo de agentes. Como se puede observar en el subprograma leer_ventas_2, la nica diferencia con el programa para un nmero jo de agentes es que ahora el nmero de elementos a leer no viene dado por un valor jo, sino por el valor ledo al principio de la secuencia de entrada. Ntese cmo, para asegurar que el programa no intenta trabajar con ms datos de los previstos, al leer el nmero de agentes comprobamos que no sea errneo, avisando con un mensaje de error adecuado en caso de ser necesario.
// ----------------------------------void leer_ventas_2 (Ventas& v) { unsigned nag; cout << "Introduzca total de agentes: "; cin >> nag; if (nag > v.size()) { v.nelms = 0; cout << "Error" << endl; } else { v.nelms = nag; for (unsigned i = 0; i < v.nelms; ++i) { cout << "Introduzca ventas del agente " << v.nelms + 1 << ": "; cin >> v.elm[i]; } } } // -- Principal -------int main () { Ventas ventas; leer_ventas(ventas); imprimir_sueldos(ventas); }

6.4.4.

Agregados Multidimensionales

Hasta ahora hemos tratado con arrays de una dimensin. En este caso podemos hacernos la idea de que el array es como un casillero en el que podemos utilizar un ndice para acceder a una determinada posicin. Ello se corresponde con la idea de una estructura lineal, con una dimensin en el espacio. Sin embargo, sabemos que el tipo base de un array puede ser tanto simple como compuesto, incluyendo la posibilidad de que sea un array. De esta forma, podemos tener arrays con mltiples dimensiones . Si cada elemento de un array es otro array, ello puede ser considerado como
Dpto. Lenguajes y Ciencias de la Computacin Universidad de Mlaga

6.4. AGREGADOS: EL TIPO ARRAY

75

un array bidimensional, coincidiendo con la idea espacial de supercie o con la idea de tabla. En este caso, necesitaramos dos ndices para acceder a cada elemento individual del array (el primero para hacer referencia a la la y el segundo para la columna). De igual forma, si este elemento es a su vez un array, obtendramos un array de tres dimensiones, lo que coincide con la idea espacial de volumen, necesitando tres ndices para acceder a cada elemento individual. Podramos continuar de igual forma, aunque ya no disponemos de una idea espacial que lo represente el valor denido. En el siguiente ejemplo se dene el tipo Matriz, que representa arrays de dos dimensiones cuyos elementos bsicos son de tipo int. Para ello, denimos el tipo Fila como un array unidimensional de int, y el tipo Matriz como un array de las.
#include <iostream> #include <array> using namespace std; // -- Constantes ------const unsigned NFILAS = 3; const unsigned NCOLUMNAS = 5; // -- Tipos -----------typedef array<int, NCOLUMNAS> Fila ; typedef array<Fila, NFILAS> Matriz ; // -- Principal -------int main () { Matriz m; for (unsigned f = 0; f < m.size(); ++f) { for (unsigned c = 0; c < m[f].size(); ++c) { m[f][c] = (f * 10) + c; } } }

0 1 2

m: 00 10 20
0

01 11 21
1

02 12 22
2

03 13 23
3

04 14 24
4

Una vez denida una variable m de tipo Matriz, su procesamiento puede requerir trabajar con una la completa, en cuyo caso utilizaramos un nico ndice. Por ejemplo m[0] hace referencia a la componente de ndice 0 de la matriz m que, segn la denicin, es de tipo Fila. As mismo, podemos estar interesados en procesar un elemento concreto de tipo int, en cuyo caso necesitamos dos ndices. Por ejemplo, m[0][2] hace referencia a la componente de ndice 2 dentro de la la 0, que es de tipo int. Del mismo modo, m.size() representa el nmero de las de la matriz m, y m[f].size() representa el nmero de elementos de la la f de la matriz m. Ejemplo. Procesamiento bsico de arrays bidimensionales A continuacin presentamos un ejemplo sencillo de procesamiento de arrays bidimensionales. Pretendemos leer de teclado nmeros correspondientes a una matriz de 3 5, efectuar algunas operaciones con la matriz y mostrar en pantalla los resultados, de acuerdo a un determinado formato. Usaremos un array bidimensional para almacenar los nmero ledos y haremos una lectura suponiendo que los nmeros son introducidos la a la. Finalmente, imprimiremos la matriz segn el siguiente formato: a a a c a a a c a a a c a a a c a a a c b b b

donde a representa los elementos de la matriz leda desde el teclado, b representa el resultado de sumar todos los elementos de la la correspondiente, y c representa el resultado de sumar todos los elementos de la columna en que se encuentran. El programa utiliza la denicin del tipo Matriz introducido anteriormente, y utiliza diversos subprogramas para efectuar cada una de las operaciones requeridas con la matriz.
Dpto. Lenguajes y Ciencias de la Computacin Universidad de Mlaga

76
#include <iostream> using namespace std; // -- Constantes ------const unsigned NFILAS = 3; const unsigned NCOLUMNAS = 5; // -- Tipos -----------typedef array<int, NCOLUMNAS> Fila ; typedef array<Fila, NFILAS> Matriz ;

CAPTULO 6. TIPOS COMPUESTOS

La funcin sumar_fila recibe como parmetro una la de la matriz y calcula la suma de sus elementos. Aunque el programa principal manipula una matriz bidimensional, el valor utilizado como parmetro actual en la llamada (m[f]) es un elemento de la matriz que, al ser de tipo Fila, encaja en la denicin del parmetro formal correspondiente. Como se puede apreciar, dentro del subprograma trabajamos con fil que es de tipo Fila, por lo que para acceder a los nmeros a sumar utilizamos un nico ndice. En realidad, sumar_fila procesa un array de una dimensin, independientemente de que la llamada sea una la de una matriz, o simplemente un vector.
// -- Subalgoritmos ---int sumar_fila (const Fila& fil) { int suma = 0; for (unsigned c = 0; c < fil.size(); ++c) { suma += fil[c]; } return suma; }

La funcin sumar_columna no puede recibir como parmetro una nica columna. El programa no contiene la denicin de ningn tipo que corresponda con lo que nosotros entendemos por una columna. Ello es algo que nos imaginamos al pensar en la matriz, pero que no est denido en el programa. Por tanto, la denicin de sumar_columna necesita toda la informacin necesaria para acceder a los elementos de una determinada columna. Es decir, la matriz completa y un nmero que indica la columna cuyos elementos queremos sumar. Como se puede apreciar, dentro del subprograma trabajamos con m que es de tipo Matriz, por lo que para acceder a los nmeros a sumar utilizamos dos ndices.
int sumar_columna (const Matriz& m, unsigned c) { int suma = 0; for (unsigned f = 0; f < m.size(); ++f) { suma += m[f][c]; } return suma; }

Al igual que ocurre con sumar_fila, para escribir una la de la matriz podemos aprovechar que existe un tipo Fila, por lo que este subprograma no es ms que la escritura en pantalla de un array de una dimensin.
void escribir_fila (const Fila& fil) { for (unsigned c = 0; c < fil.size(); ++c) { cout << fil[c] << " "; } }

Adems, como hemos denido numerosos subprogramas de apoyo, vemos que el procesamiento del array bidimensional completo queda reducido a un recorrido tpico de un array, accediendo a una la completa (m[f]) cada vez que queremos procesar una la para escribirla en pantalla o para calcular su suma.
Dpto. Lenguajes y Ciencias de la Computacin Universidad de Mlaga

6.5. RESOLUCIN DE PROBLEMAS UTILIZANDO TIPOS COMPUESTOS. AGENDA


void escribir_matriz_formato (const Matriz& m) { for (unsigned f = 0; f < m.size(); ++f) { escribir_fila(m[f]); cout << sumar_fila(m[f]); cout << endl; } for (unsigned c = 0; c < m[0].size(); ++c) { cout << sumar_columna(m, c) << " "; } cout << endl; }

77

Finalmente, aunque tambin podramos haber denido un subprograma leer_fila para leer una la, e implementar la operacin leer_matriz, haciendo llamadas a dicha operacin, a continuacin mostramos otro posible enfoque. Ahora implementamos la lectura de elementos la a la y su almacenamiento en la matriz mediante dos bucles anidados. De esta forma, ahora utilizamos dos ndices para acceder a la casilla en la que almacenar el nmero ledo.
void leer_matriz (Matriz& m) { cout << "Escribe fila a fila" << endl; for (unsigned f = 0; f < m.size(); ++f) { for (unsigned c = 0; c < m[0].size(); ++c) { cin >> m[f][c]; } } } // -- Principal -------int main () { Matriz m; leer_matriz(m); escribir_matriz_formato(m); }

6.5.

Resolucin de Problemas Utilizando Tipos Compuestos. Agenda

Finalizaremos el tema resolviendo un problema completo en el que integramos los elementos fundamentales tratados en el captulo. Para ello, disearemos un programa para gestionar una agenda personal en la que almacenaremos el nombre, el telfono y la direccin de un nmero variable de personas. El programa ofrecer un men con las opciones tpicas para manipular una agenda:
Aadir los datos de una persona. Acceder a los datos de una persona a partir de su nombre. Borrar una persona a partir de su nombre. Modicar los datos de una persona a partir de su nombre. Listar el contenido completo de la agenda.

Necesitamos denir un tipo array en el que almacenar la informacin de las personas de la agenda. Dicha denicin requiere conocer el nmero mximo de elementos del array, por lo que asumiremos que nuestra agenda no contendr ms de 50 contactos. Este problema encaja dentro de lo que hemos denominado tratamiento de arrays incompletos, porque el nmero de posiciones realmente utilizadas del array puede variar y no coincide con el nmero total de elementos del array.
Dpto. Lenguajes y Ciencias de la Computacin Universidad de Mlaga

78

CAPTULO 6. TIPOS COMPUESTOS

Al principio no habr ningn contacto y, conforme se vayan aadiendo y eliminado contactos, el nmero de posiciones ocupadas ir variando. Por este motivo, deniremos un tipo Agenda como un registro con dos campos: uno de ellos contiene el nmero actual de contactos almacenados y otro el array con la informacin detallada de los mismos. De cada contacto almacenamos su nombre, su direccin y su telfono, por lo que conviene agrupar todos estos elementos en un tipo comn. Para ello, denimos el tipo Persona como un registro con tres campos: su nombre y su telfono, de tipo string, y su direccin. Aunque se podra haber optado por denir el telfono como un campo de tipo unsigned, hemos preferido hacerlo de tipo string porque no pensamos manipularlo con operaciones aritmticas sino con operaciones de cadenas (por ejemplo, podramos pensar en obtener la subcadena que determina el prejo de la provincia). La direccin de una persona vendr dada por una calle, un piso, un cdigo postal y una ciudad. Por ese motivo, deniremos un nuevo tipo registro, llamado Direccion, que lo represente. Ntese que estamos trabajando con un registro (de tipo Persona) en el que, a su vez, uno de sus campos es otro registro (de tipo Direccion).
#include <iostream> #include <string> #include <cassert> #include <array> using namespace std ; // -- Constantes ------const unsigned MAX_PERSONAS = 50 ; // -- Tipos -----------struct Direccion { unsigned num ; string calle ; string piso ; string cp ; string ciudad ; } ; struct Persona { string nombre ; string tel ; Direccion direccion ; } ; typedef array<Persona, MAX_PERSONAS> Personas ; struct Agenda { unsigned n_pers ; Personas pers ; } ;

Se ha denido el tipo enumerado Cod_Error para denir valores que representen las posibles situaciones de error en el programa. Como veremos ms adelante, usaremos valores dicho tipo para determinar si una operacin se ha realizado correctamente (OK) o por el contrario se ha producido alguna situacin de error al ejecutar el programa.
enum Cod_Error { OK, AG_LLENA, NO_ENCONTRADO, YA_EXISTE } ;

Hemos denido una serie de subprogramas que nos permiten descomponer el programa en operaciones independientes. Utilizamos el subprograma inicializar para obtener una agenda que est vaca (es decir, que no contenga ningn elemento). Dado que hemos seguido el criterio de organizar los elementos de la agenda situndolos contiguos y al principio, y de usar un campo con el nmero total de elementos, nos bastar con hacer que el campo n_pers tome el valor cero. Ntese que, aunque parezca que esta operacin tiene poca entidad como para separarla en un subprograma independiente, en realidad ocurre justamente lo contrario. Se trata de una operacin
Dpto. Lenguajes y Ciencias de la Computacin Universidad de Mlaga

6.5. RESOLUCIN DE PROBLEMAS UTILIZANDO TIPOS COMPUESTOS. AGENDA

79

tpica a realizar con la agenda, que debe ser tratada de forma independiente. Ello permite que, por ejemplo, si en un futuro deseamos cambiar la implementacin de la agenda, los cambios puedan ser fcilmente localizados y realizados de forma segura. Tambin hemos utilizado subprogramas que permitan abordar de forma fcil y aislada la entrada/salida de los diferentes tipos denidos por el usuario. Ello suele ser una buena prctica en general, y es especialmente adecuado en este problema. Como resultado, disponemos de operaciones para leer_direccion, escribir_direccion, leer_persona y escribir_persona.
// -- Subalgoritmos ---void inicializar (Agenda& ag) { ag.n_pers = 0 ; } void leer_direccion(Direccion& dir) { cin >> dir.calle ; cin >> dir.num ; cin >> dir.piso ; cin >> dir.cp ; cin >> dir.ciudad ; } void escribir_direccion(const Direccion& dir) { cout << dir.calle << " " ; cout << dir.num << " " ; cout << dir.piso << " " ; cout << dir.cp << " " ; cout << dir.ciudad << " " ; } void leer_persona(Persona& per) { cin >> per.nombre ; cin >> per.tel ; leer_direccion(per.direccion) ; } void escribir_persona(const Persona& per) { cout << per.nombre << " " ; cout << per.tel << " " ; escribir_direccion(per.direccion) ; cout << endl ; }

Al tratar con colecciones de datos surge frecuentemente la necesidad de localizar un dato determinado para procesarlo de alguna forma. Nosotros tratamos las colecciones de datos como parte de agregados o arrays, por lo que una operacin para buscar en qu posicin del array se encuentra un determinado elemento, resulta especialmente til. Utilizamos un funcin buscar_persona que, dada una agenda y el nombre de una persona, nos indica en qu posicin se encuentra, o bien que no se encuentra. Para ello, supondremos que si el valor devuelto corresponde a una posicin del array con un dato vlido ello indica que se encuentra en dicha posicin, mientras que si corresponde a una posicin no vlida es porque no se encuentra.
unsigned buscar_persona(const string& nombre, const Agenda& ag) { unsigned i = 0 ; while ((i < ag.n_pers) && (nombre != ag.pers[i].nombre)) { ++i ; } return i ;
Dpto. Lenguajes y Ciencias de la Computacin Universidad de Mlaga

80
}

CAPTULO 6. TIPOS COMPUESTOS

Utilizamos los subprograma anyadir_persona, borrar_persona y modificar_persona para proporcionar las operaciones bsicas con las que se aade, elimina o modica informacin de la agenda. Estos subprogramas se apoyan en anyadir y eliminar, que se encargan especcamente de incluir un elemento nuevo en la agenda y de borrar el elemento de una posicin dada. La implementacin de la agenda no requiere que los elementos se encuentren en un orden concreto, por lo que el elemento a aadir se situar al nal de la lista de elementos vlidos. As mismo, para eliminar el elemento situado en una cierta posicin nicamente debemos garantizar que los elementos vlidos restantes estn situados de forma consecutiva, por lo que trasladaremos el elemento situado al nal a la posicin del hueco generado en la posicin del elemento borrado. Algunos subprogramas devuelven un parmetro ok, de salida, que se utiliza para considerar posibles situaciones de error. Por ejemplo, podra ocurrir que el usuario deseara borrar una persona que no se encuentra en la agenda. Para detectar y reaccionar ante esta situacin, dichos subprogramas devuelven un valor de tipo Cod_Error que indica la posible situacin de error. En el ejemplo citado, la operacin borrar_persona devuelve el valor OK si la operacin se pudo ejecutar correctamente y el valor NO_ENCONTRADO si no se pudo ejecutar (porque la persona no se encontraba almacenada en la agenda). Una vez que la funcin principal dispone del valor que informa si se ha producido un error, ste es usado en la llamada a escribir_cod_error. Dicho subprograma es invocado cada vez que el usuario utiliza el men para ejecutar una operacin de la agenda. Se encarga de informar de si la operacin se pudo ejecutar con normalidad o, por el contrario, se produjo alguna situacin de error.
void anyadir(Agenda& ag, const Persona& per) { ag.pers[ag.n_pers] = per ; ++ag.n_pers ; } void eliminar (Agenda& ag, unsigned pos) { if (pos < ag.npers-1) { ag.pers[pos] = ag.pers[ag.n_pers - 1] ; } --ag.n_pers ; } void anyadir_persona(const Persona& per, Agenda& ag, Cod_Error& ok) { unsigned i = buscar_persona(per.nombre, ag) ; if (i < ag.n_pers) { ok = YA_EXISTE ; } else if (ag.n_pers >= ag.pers.size()) { ok = AG_LLENA ; } else { ok = OK ; anyadir(ag, per) ; } } void borrar_persona(const string& nombre, Agenda& ag, Cod_Error& ok) { unsigned i = buscar_persona(nombre, ag) ; if (i >= ag.n_pers) { ok = NO_ENCONTRADO ; } else { ok = OK ; eliminar(ag, i) ; } } void modificar_persona(const string& nombre, const Persona& nuevo, Agenda& ag,
Dpto. Lenguajes y Ciencias de la Computacin Universidad de Mlaga

6.5. RESOLUCIN DE PROBLEMAS UTILIZANDO TIPOS COMPUESTOS. AGENDA


Cod_Error& ok) { unsigned i = buscar_persona(nombre, ag) ; if (i >= ag.n_pers) { ok = NO_ENCONTRADO ; } else { eliminar(ag, i) ; anyadir_persona(nuevo, ag, ok) ; } }

81

Los subprogramas imprimir_persona e imprimir_agenda presentan en pantalla informacin solicitada por el usuario. Ntese que, aunque en imprimir_agenda no es posible que se produzca ninguna situacin de error, tambin se utiliza un parmetro ok, en el que siempre se devuelve OK. Ello permite utilizar la operacin imprimir_agenda desde el men principal homogneamente con el resto de operaciones del usuario.
void imprimir_persona(const string& nombre, const Agenda& ag, Cod_Error& ok) { unsigned i = buscar_persona(nombre, ag) ; if (i >= ag.n_pers) { ok = NO_ENCONTRADO ; } else { ok = OK ; escribir_persona(ag.pers[i]) ; } } void imprimir_agenda(const Agenda& ag, Cod_Error& ok) { for (unsigned i = 0 ; i < ag.n_pers ; ++i) { escribir_persona(ag.pers[i]) ; } ok = OK ; }

Los subprogramas menu y escribir_cod_error se encargan de la interaccin con el usuario, bien para seleccionar la opcin adecuada o para presentar el estado resultante de ejecutar cada opcin.
char menu () { char opcion ; cout << endl ; cout << "a. - Aadir Persona" << endl ; cout << "b. - Buscar Persona" << endl ; cout << "c. - Borrar Persona" << endl ; cout << "d. - Modificar Persona" << endl ; cout << "e. - Imprimir Agenda" << endl ; cout << "x. - Salir" << endl ; do { cout << "Introduzca Opcin: " ; cin >> opcion ; } while ( ! (((opcion >= a) && (opcion <= e)) || (opcion == x))) ; return opcion ; } void escribir_cod_error (Cod_Error cod) { switch (cod) { case OK:
Dpto. Lenguajes y Ciencias de la Computacin Universidad de Mlaga

82

CAPTULO 6. TIPOS COMPUESTOS


cout << "Operacin correcta" << endl ; break ; case AG_LLENA: cout << "Agenda llena" << endl ; break ; case NO_ENCONTRADO: cout << "La persona no se encuentra en la agenda" << endl ; break ; case YA_EXISTE: cout << "La persona ya se encuentra en la agenda" << endl ; break ; } }

El programa principal se reduce a la utilizacin de los subprogramas denidos previamente. Ntese que, dado que la manipulacin de la agenda se hace en el subprograma que corresponda, la implementacin concreta de la agenda est oculta para el programa principal, por lo que queda claramente legible y fcilmente modicable.
// -- Principal -------int main () { Agenda ag ; char opcion ; Persona per ; string nombre ; Cod_Error ok ; inicializar(ag) ; do { opcion = menu() ; switch (opcion) { case a: cout << "Introduzca los datos de la Persona" << endl ; cout << "(nombre, tel, calle, num, piso, cod_postal, ciudad)" << endl ; leer_Persona(per) ; anyadir_persona(per, ag, ok) ; escribir_cod_error(ok) ; break ; case b: cout << "Introduzca Nombre" << endl ; cin >> nombre ; imprimir_persona(nombre, ag, ok) ; escribir_cod_error(ok) ; break ; case c: cout << "Introduzca Nombre" << endl ; cin >> nombre ; borrar_persona(nombre, ag, ok) ; escribir_cod_error(ok) ; break ; case d: cout << "Introduzca Nombre" << endl ; cin >> nombre ; cout << "Nuevos datos de la Persona" << endl ; cout << "(nombre, tel, calle, num, piso, cod_postal, ciudad)" << endl ; leer_persona(per) ; modificar_persona(nombre, per, ag, ok) ; escribir_cod_error(ok) ; break ;
Dpto. Lenguajes y Ciencias de la Computacin Universidad de Mlaga

6.5. RESOLUCIN DE PROBLEMAS UTILIZANDO TIPOS COMPUESTOS. AGENDA


case e: imprimir_agenda(ag, ok) ; escribir_cod_error(ok) ; break ; } } while (opcion != x ) ; }

83

Dpto. Lenguajes y Ciencias de la Computacin

Universidad de Mlaga

84

CAPTULO 6. TIPOS COMPUESTOS

Dpto. Lenguajes y Ciencias de la Computacin

Universidad de Mlaga

Captulo 7

Bsqueda y Ordenacin
Al desarrollar programas es frecuente que nos encontremos con situaciones en las que es necesario acceder a (buscar ) un determinado elemento de una coleccin con un objetivo concreto. Podemos estar interesados, por ejemplo, en mostrarlo en pantalla, en eliminarlo o en modicarlo segn resulte conveniente. En el captulo anterior ya nos percatamos de ello al desarrollar nuestro programa para gestionar una agenda de contactos. La gran importancia de este tipo de operaciones de bsqueda hace que nos planteemos su estudio de forma ms detallada. A continuacin mostraremos algunas posibilidades para abordar la bsqueda. Hay otras posibilidades, como el uso de tcnicas de bsqueda en tablas hash que, aunque son de gran utilidad, no trateremos en este captulo. Hasta ahora hemos tratado con colecciones de elementos almacenadas en arrays, pero no nos hemos preocupado de la disposicin interna de dichos elementos en el array. Lo importante ha sido que los elementos se encuentren almacenados en el array y que podamos acceder a ellos, pero sin imponer un criterio de almacenamiento concreto. Sin embargo, hay determinadas ocasiones en los que podemos estar interesados en que los elementos se encuentren organizados de acuerdo a un determinado criterio. Por ejemplo, podramos estar interesados en que los elementos se almacenen en la agenda ordenados ascendentemente por el nombre de la persona. En este captulo revisaremos algunas estrategias tpicas para ordenar los elementos en un array. A continuacin, asumiremos que la coleccin de elementos con la que trabajamos (tanto para bsqueda como para ordenacin) se encuentra almacenada en un array, para lo que usaremos el siguiente tipo Vector.
//-------------------------------const unsigned MAXIMO = 50; typedef array<int, MAXIMO> Vector ;

7.1.

Algoritmos de Bsqueda

En general, si deseamos localizar un elemento en un array es porque despus queremos hacer algo con dicho elemento. Por este motivo, disearemos nuestros algoritmos de bsqueda de forma que devuelvan la posicin del elemento que queremos localizar, o bien una indicacin de que el elemento buscado no se se encuentra en la coleccin.
//-------------------------------// busca la posicin del primer elemento igual a x // si no se encuentra, retorna v.size() //------------unsigned buscar(int x, const Vector& v);

Hay diferentes criterios para reconocer que un elemento no se encuentra en la coleccin. En general, basta con obtener una posicin no vlida. Nosotros supondremos que esta posicin no 85

86

CAPTULO 7. BSQUEDA Y ORDENACIN

vlida es el ndice de un elemento del array mayor que el ltimo elemento de la coleccin (usaremos v.size()).

7.1.1.

Bsqueda Lineal (Secuencial)

Consiste en ir recorriendo secuencialmente la coleccin de datos hasta encontrar el elemento buscado o, en ltima instancia, hasta recorrer la coleccin completa, en cuyo caso podemos estar seguros de que el elemento buscado no se encuentra en la coleccin. La bsqueda lineal es adecuada como mecanismo de bsqueda general en colecciones de datos sin organizacin conocida. A continuacin se muestra el algoritmo bsico, que puede ser adaptado segn las circunstancias.
//-------------------------------// busca la posicin del primer elemento igual a x // si no se encuentra, retorna v.size() //------------unsigned buscar(int x, const Vector& v) { unsigned i = 0 ; while ((i < v.size())&&(x != v[i])) { ++i ; } return i ; } //--------------------------------

Como puede observarse, recorremos uno a uno todos los elementos hasta que podemos responder en sentido armativo o negativo. Respondemos en sentido negativo (el elemento no se encuentra) si el ndice del siguiente elemento a probar est ms all del ltimo elemento del array. Respondemos en sentido positivo si el elemento indicado por la variable i contiene el elemento buscado. En tal caso, acaba el bucle y se devuelve dicha posicin i. Ntese que si el elemento no se encuentra se devuelve v.size(), que es una posicin no vlida del array. Un programa que haga uso de la funcin buscar usar el valor devuelto para determinar si el elemento a buscar se encuentra o no en el array. Por ejemplo, el siguiente fragmento elimina un elemento del array en caso de que se encuentre y controla las situaciones de error que se puedan dar.
unsigned p = buscar(num, v) ; if (p < v.size()){ eliminar(v, p) ; cod_err = OK ; }else{ cod_err = NO_ENCONTRADO ; }

El acceso a elementos de un array exige estar seguro de que no accedemos a elementos fuera de los ndices denidos para el mismo. Por este motivo, es importante que el orden en el que se evalan las diferentes condiciones en la expresin de control del n del bucle sea el mostrado en el algoritmo. De esa forma, aprovechamos la evaluacin en cortocircuito y garantizamos que el bucle se detiene cuando no hay ms elementos a inspeccionar (i >= v.size()), evitando as accesos errneos a posiciones no vlidas del array. Si hubiramos escrito el bucle permutando las dos partes de la expresin de control del bucle:
while ((x != v[i])&&(i < v.size())) {

tendramos un algoritmo incorrecto, porque si el elemento no se encuentra, intentaramos acceder posiciones del array que no forman parte del mismo.
Dpto. Lenguajes y Ciencias de la Computacin Universidad de Mlaga

7.1. ALGORITMOS DE BSQUEDA Bsqueda Lineal Multidimensional 2D

87

Al igual que la bsqueda lineal considerada anteriormente, la bsqueda lineal multidimensional 2D consiste en ir recorriendo secuencialmente la coleccin de datos (en este caso estructurada en dos dimensiones) hasta encontrar el elemento buscado o, en ltima instancia, hasta recorrer la coleccin completa, en cuyo caso podemos estar seguros de que el elemento buscado no se encuentra en la coleccin.
const unsigned NCOLS = 5 ; const unsigned NFILS = 7 ; typedef array<int, NCOLS> Fila ; typedef array<Fila, NFILS> Matriz ; //-------------------------------// busca la posicin del primer elemento igual a x // si no se encuentra, f tomar el valor v.size() // y c un valor inespecificado //------------void buscar(int x, const Matriz& m, unsigned& f, unsigned& c) { f = 0 ; c = 0 ; while ((f < m.size())&&(x != m[f][c])) { ++c ; if (c >= m[f].size()) { c = 0 ; ++f ; } } } //--------------------------------

7.1.2.

Bsqueda Binaria

La bsqueda secuencial es muy simple, pero requiere recorrer todos los elementos del array para estar seguros de que el elemento a buscar no se encuentra en la coleccin. Si el array tiene gran cantidad de elementos y necesitamos que las bsquedas se realicen de forma rpida, este mecanismo podra no ser adecuado. Si estamos interesados en acelerar el proceso de bsqueda, necesitamos informacin adicional que nos de pistas para poder realizar una implementacin ms eciente. Necesitamos tener informacin acerca de la organizacin interna de los elementos en el array. Hay diferentes formas de organizar la informacin en el array que nos permiten implemementar algoritmos de bsqueda ms ecientes. A continuacin estudiaremos el algoritmo de bsqueda binaria, que asume que las colecciones de datos se encuentran almacenadas en el array de forma ordenadas segn algn criterio. La idea consiste en seleccionar un elemento de la coleccin y comprobar si se trata del elemento buscado. Si es as el proceso termina con xito, pero si no lo es, podemos aprovechar que sabemos que los elementos se encuentran ordenados y descartar todos los elementos que se encuentran a la derecha del mismo o a su izquierda (segn la relacin entre el valor seleccionado y el valor buscado). Este proceso se repite hasta encontrar el elemento o hasta que no queden elementos en la coleccin, en cuyo caso el elemento no habr sido encontrado. A continuacin se presenta el algoritmo bsico, que puede ser adaptado segn el contexto concreto en el que se quiera utilizar.
//-------------------------------// busca la posicin del primer elemento igual a x // si no se encuentra, retorna v.size() //------------unsigned buscar_bin(int x, const Vector& v) {
Dpto. Lenguajes y Ciencias de la Computacin Universidad de Mlaga

88
unsigned i = 0 ; unsigned f = v.size() ; unsigned res = v.size() ; unsigned m = (i + f) / 2 ; while ((i < f) && (x ! v[m])) { if (x < v[m]) { f = m ; } else { i = m + 1 ; } m = (i + f) / 2 ; } if (i < f) { res = m ; } return res ; } //--------------------------------

CAPTULO 7. BSQUEDA Y ORDENACIN

Como se puede observar, utilizamos dos ndices (i y f) para delimitar la zona del array con elementos entre los que buscar. El ndice i (inicio) indica el primer elemento vlido del array y el ndice f (n) indica el primer elemento no vlido. Al principio, la zona coincide con el array completo, por lo que hacemos que i tome el valor 0 y f tome el valor v.size() (el primero no vlido). Mientras queden elementos entre los que buscar (i < f), seleccionamos uno con el que probar. Lo ms ptimo es seleccionar el central ((i + f) / 2), porque de esa forma descartamos un mayor nmero de elementos, en caso de que el elemento seleccionado no sea el buscado. En cada paso probamos con el elemento seleccionado y actualizamos los ndices adecuadamente como consecuencia de la comparacin entre el elemento seleccionado y el buscado. Salimos del bucle si encontramos el elemento en la posicin m o si los ndices i y f se cruzan (indicando que no quedan elementos entre los que seguir buscando). Al salir del bucle nos aseguramos que la variable res contenga el valor adecuado, es decir, o bien un valor fuera del rango vlido (ntese que se inicializa a v.size()), o bien al valor m con la posicin del elemento buscado.

7.2.

Algoritmos de ordenacin

El problema de cmo ordenar los elementos en un array aparece frecuentemente en la bibliografa bsica de programacin, bien como base para proponer algoritmos tpicos sobre los que estudiar propiedades de los programas o como objeto de estudio para obtener algoritmos ecientes que reduzcan el coste computacional del proceso de ordenacin. Nosotros utilizamos este problema como parte de nuestro aprendizaje de programacin bsica, por lo que presentaremos algunos de los algoritmos ms tpicos, sin preocuparnos de buscar soluciones especialmente ecientes, sino buscando soluciones claras, simples y fciles de entender. Para encontrar propuestas ms ecientes debe consultarse bibliografa ms avanzada.

7.2.1.

Ordenacin por Seleccin

La idea de este algoritmo consiste en buscar el menor elemento de aquellos a ordenar y situarlo en su posicin (al principio). De esta forma, la coleccin a ordenar pasa a tener un elemento menos, y basta con repetir el proceso, pero considerando una coleccin desordenada menor. El proceso termina cuando la coleccin a ordenar contiene un nico elemento. El siguiente subprograma seleccion muestra nuestra solucin, que describe exactamente la idea presentada anteriormente. Se basa en el uso de un subprograma subir_menor, que considera un array sin ordenar y sita el menor elemento al principio del mismo. Haciendo uso de este subprograma, basta con realizar sucesivas iteraciones en las que usamos subir_menor identicando los elementos del array que quedan sin ordenar. Para ello, el subprograma subir_menor recibe,
Dpto. Lenguajes y Ciencias de la Computacin Universidad de Mlaga

7.2. ALGORITMOS DE ORDENACIN

89

adems del array a ordenar, el ndice (pos) que delimita la parte del mismo que est aun sin ordenar.
//-------------------------------void seleccion(Vector& v) { for (unsigned pos = 0 ; pos < v.size()-1 ; ++pos) { subir_menor(v, pos) ; } } //--------------------------------

El subprograma subir_menor recibe una zona de un array, marcada desde un cierto elemento pos hasta el nal, y sita el menor elemento al principio. Si es necesario, el elemento que ocupaba la primera posicin es situado en la posicin que ocupaba el menor. Necesitamos localizar la posicin del menor elemento del array, por lo que utilizamos un subprograma posicion_menor.
//-------------------------------inline void subir_menor(Vector& v, unsigned pos) { unsigned pos_menor = posicion_menor(v, pos) ; if (pos != pos_menor) { intercambio(v[pos], v[pos_menor]) ; } }

Finalmente, solo queda proceder a la implementacin de los correspondientes subprogramas para localizar el menor elemento de un array y para intercambiar dos elementos.
//-------------------------------inline void intercambio(int& x, int& y) { int a = x ; x = y ; y = a ; } //-------------------------------unsigned posicion_menor(const Vector& v, unsigned pos) { int pos_menor = pos ; for (unsigned i = pos_menor+1 ; i < v.size() ; ++i) { if (v[i] < v[pos_menor]) { pos_menor = i ; } } return pos_menor ; }

7.2.2.

Ordenacin por Intercambio (Burbuja)

La idea de este algoritmo consiste en hacer repetidas pasadas sobre el array, trasladando en cada una el elemento ms pequeo hasta el principio del array. Este algoritmo se conoce como ordenacin por el mtodo de la burbuja, porque si se consideran los elementos como si estuviera en posicin vertical y fueran burbujas con un cierto peso en un depsito de agua, dichas burbujas iran ascendiendo en funcin de su valor. Como se puede apreciar, al igual que en el algoritmo de ordenacin por seleccin, tambin tenemos sucesivas pasadas en las que conseguimos situar un elemento al principio. La diferencia est en el mecanismo utilizado para conseguirlo. Por ello, el subprograma burbuja sigue el mismo esquema que el subprograma seleccion presentado anteriormente, salvo que ahora la implementacin del subprograma subir_menor es diferente, respondiendo a la idea de burbuja.
Dpto. Lenguajes y Ciencias de la Computacin Universidad de Mlaga

90

CAPTULO 7. BSQUEDA Y ORDENACIN


//-------------------------------void subir_menor(Vector& v, unsigned pos) { for (unsigned i = v.size()-1 ; i > pos ; --i) { if (v[i] < v[i-1]) { intercambio(v[i], v[i-1]) ; } } } //-------------------------------void burbuja(Vector& v) { for (int pos = 0 ; pos < v.size()-1 ; ++pos) { subir_menor(v, pos) ; } } //--------------------------------

7.2.3.

Ordenacin por Insercin

En este caso, la idea consiste en cosiderar dos zonas en el array, la primera contiene elementos ordenados y la segunda contiene elementos desordenados. En cada paso se toma un elemento de la zona desordenada y se inserta en la zona de elementos ordenados, de forma que quede en la posicin adecuada. Conseguimos que la zona ordenada tenga un elemento ms y la desordenada un elemento menos. El proceso contina hasta que la zona desordenada no contiene ningn elemento. En nuestra implementacin usamos pos para delimitar el comienzo de la zona de elementos desordenados. Todos los elementos a su izquierda estarn ordenados y en cada paso se insertar el elementos indicado por pos en la zona ordenada del array. Ntese cmo hacemos que pos tome inicialmente el valor 1. Ello representa la situacin inicial, en la que la zona ordenada contiene un nico elemento y la desordenada el resto. En cada iteracin localizamos la posicin en la que debemos situar el elemento tratado (v[pos]) y procedemos a su insercin. Si al elemento a insertar le corresponde ir al nal de la zona ordenada, ya se encuentra en su posicin correcta, por lo que no habra que hacer nada. Sin embargo, en cualquier otro caso deberemos garantizar que, una vez situado el elemento en la posicin adecuada, todos los elementos sigan estando ordenados. Para ello, antes de almacenar el elemento en la posicin destino abrimos un hueco en dicha posicin, desplazando cada elemento una posicin a su derecha.
//-------------------------------void insercion(Vector& v) { for (unsigned pos = 1 ; pos < v.size() ; ++pos) { unsigned p_hueco = buscar_posicion(v, pos) ; if (p_hueco != pos) { int aux = v[pos] ; abrir_hueco(v, p_hueco, pos) ; v[p_hueco] = aux ; } } } //--------------------------------

Ls bsqueda de la posicin del elemento es un recorrido de bsqueda normal, en el que comprobamos la condicin en la zona del array delimitada entre 0 .. pos. Como puede observarse, en este caso sabemos que v[pos] no cumplir la condicion, por lo que no es necesario utilizar una condicin compuesta (no es necesario comprobar que se alcanza el n de la zona de bsqueda). A continuacin se muestra el algoritmo, en el que omitimos (comentamos) dicha parte de la comprobacin.
Dpto. Lenguajes y Ciencias de la Computacin Universidad de Mlaga

7.3. APLICACIN DE LOS ALGORITMOS DE BSQUEDA Y ORDENACIN


//-------------------------------unsigned buscar_posicion(const Vector& v, unsigned pos) { unsigned i = 0 ; while (/*(i < pos)&&*/ (v[pos] > v[i])) { ++i ; } return i ; }

91

El subprograma abrir_hueco desplaza uno a uno los elementos en una zona dada, avanzando de derecha a izquierda para evitar que el desplazamiento de un elemento afecte al siguiente.
//-------------------------------void abrir_hueco(Vector& v, unsigned p_hueco, unsigned p_elm) { for (unsigned i = p_elm ; i > p_hueco ; --i) { v[i] = v[i-1] ; } }

7.3.

Aplicacin de los Algoritmos de Bsqueda y Ordenacin

Finalmente, adaptaremos nuestra solucin al problema de la agenda personal. Ahora consideraremos que queremos que la informacin se encuentre internamente almacenada de forma ordenada. De esta forma, podremos proceder a efectuar bsquedas mediante el algoritmo de bsqueda binaria, consiguiendo as que el proceso de bsqueda sea muy eciente.
#include <iostream> #include <string> #include <cassert> #include <array> using namespace std ; // -- Constantes ------const int MAX_PERSONAS = 50 ; // -- Tipos -----------struct Direccion { unsigned num ; string calle ; string piso ; string cp ; string ciudad ; } ; struct Persona { string nombre ; string tel ; Direccion direccion ; } ; // -- Tipos -----------typedef array<Persona, MAX_PERSONAS> Personas ; struct Agenda { unsigned n_pers ; Personas pers ; } ; enum Cod_Error { OK, AG_LLENA, NO_ENCONTRADO, YA_EXISTE } ; // -- Subalgoritmos ---Dpto. Lenguajes y Ciencias de la Computacin Universidad de Mlaga

92

CAPTULO 7. BSQUEDA Y ORDENACIN


void inicializar (Agenda& ag) { ag.n_pers = 0 ; } //--------------------------void leer_direccion (Direccion& dir) { cin >> dir.calle ; cin >> dir.num ; cin >> dir.piso ; cin >> dir.cp ; cin >> dir.ciudad ; } //--------------------------void escribir_direccion (const Direccion& dir) { cout << dir.calle << " " ; cout << dir.num << " " ; cout << dir.piso << " " ; cout << dir.cp << " " ; cout << dir.ciudad << " " ; } //--------------------------void leer_persona (Persona& per) { cin >> per.nombre ; cin >> per.tel ; leer_direccion(per.direccion) ; } //--------------------------void escribir_persona (const Persona& per) { cout << per.nombre << " " ; cout << per.tel << " " ; escribir_direccion(per.direccion) ; cout << endl ; } //--------------------------// Busca una Persona en la Agenda Ordenada // Devuelve su posicin si se encuentra, o bien >= ag.n_pers en otro caso unsigned buscar_persona (const string& nombre, const Agenda& ag) { unsigned i = 0 ; unsigned f = ag.n_pers ; unsigned res = ag.n_pers ; while (i < f) { unsigned m = (i + f) / 2 ; int cmp = nombre.compare(ag.pers[m].nombre) ; if (cmp == 0) { res = m ; i = m ; f = m ; } else if (cmp < 0) { f = m ; } else { i = m + 1 ; } } return res ;

Dpto. Lenguajes y Ciencias de la Computacin

Universidad de Mlaga

7.3. APLICACIN DE LOS ALGORITMOS DE BSQUEDA Y ORDENACIN


}

93

La implementacin de buscar_persona pretende ser eciente. Por ese motivo, evitamos repetir innecesariamente la comparacin de la cadena a buscar y la cadena almacenada en la posicin estudiada en cada paso. En su lugar, hacemos una nica comparacin y almacenamos su resultado en la variable cmp1 . Posteriormente, en cada paso decidimos qu hacer en funcin del contenido de cmp. Ntese que en este ejemplo hemos utilizado un algoritmo de bsqueda binaria diferente del presentado al principio del captulo, consiguiendo as limitar el nmero de comparaciones de cadenas a realizar.
//--------------------------unsigned buscar_posicion (const string& nombre, const Agenda& ag) { unsigned i = 0 ; while ((i < ag.n_pers) && (nombre > ag.pers[i].nombre)) { ++i ; } return i ; } //--------------------------void anyadir_ord (Agenda& ag, unsigned pos, const Persona& per) { for (unsigned i = ag.n_pers ; i > pos ; --i) { ag.pers[i] = ag.pers[i - 1] ; } ag.pers[pos] = per ; ++ag.n_pers ; } //--------------------------void eliminar_ord (Agenda& ag, unsigned pos) { --ag.n_pers ; for (unsigned i = pos ; i < ag.n_pers ; ++i) { ag.pers[i] = ag.pers[i + 1] ; } } //--------------------------void anyadir_persona (const Persona& per, Agenda& ag, Cod_Error& ok) { unsigned pos = buscar_posicion(per.nombre, ag) ; if ((pos < ag.n_pers) && (per.nombre == ag.pers[pos].nombre)) { ok = YA_EXISTE ; } else if (ag.n_pers >= ag.pers.size()) { ok = AG_LLENA ; } else { ok = OK ; anyadir_ord(ag, pos, per) ; } } //--------------------------void borrar_persona (const string& nombre, Agenda& ag, Cod_Error& ok) { unsigned i = buscar_persona(nombre, ag) ; if (i >= ag.n_pers) { ok = NO_ENCONTRADO ;
1 Usamos la funcin compare de la biblioteca string, que devuelve 0 si las cadenas comparadas son iguales, un valor positivo si el argumento es lexicogrcamente menor y un valor negativo en caso contrario.

Dpto. Lenguajes y Ciencias de la Computacin

Universidad de Mlaga

94
} else { ok = OK ; eliminar_ord(ag, i) ; }

CAPTULO 7. BSQUEDA Y ORDENACIN

} //--------------------------void modificar_persona (const string& nombre, const Persona& nuevo, Agenda& ag, Cod_Error& ok) { unsigned i = buscar_persona(nombre, ag) ; if (i >= ag.n_pers) { ok = NO_ENCONTRADO ; } else { ok = OK ; eliminar_ord(ag, i) ; anyadir_persona(nuevo, ag, ok) ; } } //--------------------------void imprimir_persona (const string& nombre, const Agenda& ag, Cod_Error& ok) { unsigned i = buscar_persona(nombre, ag) ; if (i >= ag.n_pers) { ok = NO_ENCONTRADO ; } else { ok = OK ; escribir_persona(ag.pers[i]) ; } } //--------------------------void imprimir_agenda (const Agenda& ag, Cod_Error& ok) { for (int i = 0 ; i < ag.n_pers ; ++i) { escribir_persona(ag.pers[i]) ; } ok = OK ; } //--------------------------char menu () { char opcion ; cout << endl ; cout << "a. - Aadir Persona" << endl ; cout << "b. - Buscar Persona" << endl ; cout << "c. - Borrar Persona" << endl ; cout << "d. - Modificar Persona" << endl ; cout << "e. - Imprimir Agenda" << endl ; cout << "x. - Salir" << endl ; do { cout << "Introduzca Opcin: " ; cin >> opcion ; } while ( ! (((opcion >= a) && (opcion <= e)) || (opcion == x))) ; return opcion ; } //--------------------------void escribir_cod_error (Cod_Error cod) { switch (cod) { case OK:

Dpto. Lenguajes y Ciencias de la Computacin

Universidad de Mlaga

7.3. APLICACIN DE LOS ALGORITMOS DE BSQUEDA Y ORDENACIN


cout << "Operacin correcta" << endl ; break ; case AG_LLENA: cout << "Agenda llena" << endl ; break ; case NO_ENCONTRADO: cout << "La persona no se encuentra en la agenda" << endl ; break ; case YA_EXISTE: cout << "La persona ya se encuentra en la agenda" << endl ; break ; }

95

} // -- Principal -------int main () { Agenda ag ; char opcion ; Persona per ; string nombre ; Cod_Error ok ; inicializar(ag) ; do { opcion = menu() ; switch (opcion) { case a: cout << "Introduzca los datos de la Persona"<<endl ; cout << "(nombre, tel, calle, num, piso, cod_postal, ciudad)" << endl ; leer_persona(per) ; anyadir_persona(per, ag, ok) ; escribir_cod_error(ok) ; break ; case b: cout << "Introduzca Nombre" << endl ; cin >> nombre ; imprimir_persona(nombre, ag, ok) ; escribir_cod_error(ok) ; break ; case c: cout << "Introduzca Nombre" << endl ; cin >> nombre ; borrar_persona(nombre, ag, ok) ; escribir_cod_error(ok) ; break ; case d: cout << "Introduzca Nombre" << endl ; cin >> nombre ; cout << "Nuevos datos de la Persona" << endl ; cout << "(nombre, tel, calle, num, piso, cod_postal, ciudad)" << endl ; leer_persona(per) ; modificar_persona(nombre, per, ag, ok) ; escribir_cod_error(ok) ; break ; case e: imprimir_agenda(ag, ok) ; escribir_cod_error(ok) ; break ; } } while (opcion != x ) ;

Dpto. Lenguajes y Ciencias de la Computacin

Universidad de Mlaga

96
}

CAPTULO 7. BSQUEDA Y ORDENACIN

Dpto. Lenguajes y Ciencias de la Computacin

Universidad de Mlaga

Captulo 8

Algunas Bibliotecas tiles


En este captulo se muestra supercialmente algunas funciones bsicas de la biblioteca estndar.

cmath
La biblioteca <cmath> proporciona principalmente algunas funciones matemticas tiles:
#include <cmath> using namespace std ; double double double double double double double double double double double double double double double double double double double double double double sin(double r) ; cos(double r) ; tan(double r) ; asin(double x) ; acos(double x) ; atan(double x) ; atan2(double y, double x) ; sinh(double r) ; cosh(double r) ; tanh(double r) ; sqrt(double x) ; pow(double x, double y) ; exp(double x) ; log(double x) ; log10(double x) ; ceil(double x) ; floor(double x) ; fabs(double x) ; ldexp(double x, int n) ; frexp(double x, int* exp) ; modf(double x, double* ip) ; fmod(double x, double y) ; seno, sin r (en radianes) coseno, cos r (en radianes) tangente, tan r (en radianes) arco seno, arcsin x, x [1, 1] arco coseno, arc cos x, x [1, 1] arco tangente, arctan x arco tangente, arctan y/x seno hiperblico, sinh r coseno hiperblico, cosh r tangente hiperblica, tanh r x, x 0 xy ex logaritmo neperiano, ln x, x > 0 logaritmo decimal, log x, x > 0 menor entero x, x mayor entero x, x valor absoluto de x, |x| x2n inversa de ldexp parte entera y fraccionaria resto de x/y

cctype
La biblioteca <cctype> proporciona principalmente caractersticas sobre los valores de tipo char:
#include <cctype> using namespace std ;

97

98
bool bool bool bool bool bool bool bool bool bool bool char char isalnum(char ch) ; isalpha(char ch) ; iscntrl(char ch) ; isdigit(char ch) ; isgraph(char ch) ; islower(char ch) ; isprint(char ch) ; ispunct(char ch) ; isspace(char ch) ; isupper(char ch) ; isxdigit(char ch) ; tolower(char ch) ; toupper(char ch) ;

CAPTULO 8. ALGUNAS BIBLIOTECAS TILES


(isalpha(ch) || isdigit(ch)) (isupper(ch) || islower(ch)) caracteres de control dgito decimal caracteres imprimibles excepto espacio letra minscula caracteres imprimibles incluyendo espacio carac. impr. excepto espacio, letra o dgito espacio, \r, \n, \t, \v, \f letra mayscula dgito hexadecimal retorna la letra minscula correspondiente a ch retorna la letra mayscula correspondiente a ch

ctime
La biblioteca <ctime> proporciona principalmente algunas funciones generales relacionadas con el tiempo:
#include <ctime> using namespace std ; clock_t clock() ; time_t time(0) ; retorna el tiempo de CPU utilizado (CLOCKS_PER_SEC) retorna el tiempo de calendario (en segundos)

#include <iostream> #include <ctime> using namespace std ; // ------------------------------------int main() { time_t t1 = time(0) ; clock_t c1 = clock() ; // ... procesamiento ... clock_t c2 = clock() ; time_t t2 = time(0) ; cout << "Tiempo de CPU: " << double(c2 - c1)/double(CLOCKS_PER_SEC) << " seg" << endl ; cout << "Tiempo total: " << (t2 - t1) << " seg" << endl ; } // -------------------------------------

cstdlib
La biblioteca <cstdlib> proporciona principalmente algunas funciones generales tiles:
#include <cstdlib> using namespace std ;

int abs(int n) ; long labs(long n) ; int system(const char orden[]) ; void exit(int estado) ; void abort() ; void srand(unsigned semilla) ; int rand() ; #include <cstdlib> #include <ctime> using namespace std ;

retorna el valor absoluto del nmero int n retorna el valor absoluto del nmero long n orden a ejecutar por el sistema operativo termina la ejecucin del programa actual (EXIT_SUCCESS, EXIT_FAILURE) aborta la ejecucin del programa actual inicializa el generador de nmeros aleatorios retorna un aleatorio entre 0 y RAND_MAX (ambos inclusive)

Dpto. Lenguajes y Ciencias de la Computacin

Universidad de Mlaga

99
// ------------------------------------// inicializa el generador de nmeros aleatorios inline void ini_aleatorio() { srand(time(0)) ; } // ------------------------------------// Devuelve un nmero aleatorio entre 0 y max (exclusive) inline int aleatorio(int max) { return int(max*double(rand())/(RAND_MAX+1.0)) ; } // ------------------------------------// Devuelve un nmero aleatorio entre min y max (ambos inclusive) inline int aleatorio(int min, int max) { return min + aleatorio(max-min+1) ; } // -------------------------------------

Dpto. Lenguajes y Ciencias de la Computacin

Universidad de Mlaga

100

CAPTULO 8. ALGUNAS BIBLIOTECAS TILES

Dpto. Lenguajes y Ciencias de la Computacin

Universidad de Mlaga

Parte II

Programacin Intermedia

101

Captulo 9

Almacenamiento en Memoria Secundaria: Ficheros


Un programa suele trabajar con datos almacenados en la memoria principal (RAM). sta se caracteriza por proporcionar un acceso (para lectura y escritura) rpido a la informacin almacenada. Sin embargo, este tipo de memoria es voltil, en el sentido de que los datos almacenados en ella desaparecen cuando termina la ejecucin del programa o se apaga el ordenador. Por este motivo, para almacenar informacin de manera permanente se utilizan dispositivos de almacenamiento de memoria secundaria, tales como dispositivos magnticos (discos duros, cintas), discos pticos (CDROM, DVD), memorias permanentes de estado slido (memorias ash USB), etc. Los dispositivos de memoria secundaria suelen disponer de gran capacidad de almacenamiento, por lo que es necesario alguna organizacin que permita gestionar y acceder a la informacin all almacenada. A esta organizacin se la denomina el sistema de cheros, y suele estar organizado jerrquicamente en directorios (a veces denominados tambin carpetas) y cheros (a veces denominados tambin archivos). Los directorios permiten organizar jerrquicamente y acceder a los cheros, y estos ltimos almacenan de forma permanente la informacin, que puede ser tanto programas (software) como datos que sern utilizados por los programas.

raiz

bin

src

system.cfg

gedit

g++

agenda.cpp

agenda

agenda.txt

Tipos de Ficheros Los cheros se pueden clasicar atendiendo a diferentes criterios. En nuestro caso, nos centraremos en su clasicacin en funcin de la codicacin o formato en el que almacenan la informacin. As, podemos distinguir entre cheros de texto y cheros binarios. En los cheros de texto la informacin se almacena como una secuencia de caracteres y cada carcter se almacena utilizando una codicacin estndar (usualmente basada en la codicacin ASCII, UTF-8, etc). Al tratarse de un formato estandarizado, otros programas diferentes de aquel que cre el chero podrn entender y procesar su contenido. Por ejemplo, un programa podra generar un chero de texto con los datos de las personas de una agenda y posteriormente dicho chero podra ser entendido y procesado por otros programas. Por ejemplo, podra ser visualizado y editado mediante programas de edicin de textos de propsito general, tales como gedit, kate, gvim, emacs, etc. en Linux, textedit en MacOS-X y notepad en Windows, entre otros. 103

104

CAPTULO 9. ALMACENAMIENTO EN MEMORIA SECUNDARIA: FICHEROS

En los cheros binarios, la informacin se almacena con el mismo formato y codicacin utilizada para su almacenamiento en memoria principal. Estn concebidos para ser procesados automticamente por programas que conocen su formato interno. Un programa no podr procesar la informacin que contiene si no dispone de documentacin adecuada que describa su formato interno1 . Este tipo de cheros se utiliza cuando no estamos interesados en que la informacin sea visible por otros programas. El procesamiento de cheros binarios es ms eciente que el de cheros de texto porque la informacin est representada directamente en cdigo binario (exactamente tal y como se encuentra internamente en la memoria principal). De esta forma se evita la prdida de tiempo que ocasionara su conversin a un formato estndar (ASCII, UTF-8, etc), como ocurre en los cheros de texto. En el caso del software, los programas en cdigo fuente codicados en un lenguaje de programacin suelen ser almacenados como cheros de texto. Sin embargo, el resultado de compilar estos programas fuente a programas ejecutables se almacenan en cheros binarios (ejecutables por el Sistema Operativo). As mismo, los cheros que contienen imgenes, vdeo y msica suelen estar, en su mayora, almacenados en formato binario. Consideremos un ejemplo concreto y supongamos que disponemos de un chero de texto denominado fechas.txt, que podra estar almacenado en una determinada posicin en la jerarqua del sistema de cheros (/home/alumno/documentos/fechas.txt) y contener informacin sobre las fechas de nacimiento de determinadas personas segn el siguiente formato, donde cada lnea se encuentra terminada por un carcter terminador de n de lnea:2
Juan Lpez 12 3 1992 Lola Martnez 23 7 1987 Pepe Jimnez 17 8 1996

Aunque los datos almacenados en memoria se encuentran en formato binario, son convertidos a su representacin textual antes de ser escritos en el chero. Similarmente, cuando leemos del chero de texto para almacenar la informacin en memoria se produce una conversin de formato de texto a fomato binario. Por ejemplo, si el nmero 12 se almacena en una variable de tipo unsigned su representacin interna utilizara un nmero binario de 32 bits (00000000000000000000000000001100). Sin embargo, su representacin textual en el chero de texto se compone de una secuencia de dos caracteres (1 2). Como se puede observar, el valor se almacena internamente en memoria en formato binario utilizando 4 bytes, mientras que en el chero de texto se almacenan los dos caracteres con el valor que corresponda segn el cdigo ASCII (2 bytes).

9.1.

Flujos de Entrada y Salida Asociados a Ficheros

En el captulo 3 se explic que un programa codicado en C++ realiza la entrada y salida de informacin a travs de ujos (stream en ingls) de entrada y salida respectivamente. Se estudi cmo realizar la entrada y salida de datos a travs de los ujos estndares de entrada y salida (cin y cout, respectivamente), usualmente conectados con el teclado y la pantalla de la consola. Todo lo explicado anteriormente respecto a la entrada y salida bsica con los ujos estndares (captulo 3), o la entrada y salida de cadenas de caracteres (captulo 6.2.1)tambin es aplicable a los ujos de entrada y salida vinculados a cheros que veremos en este captulo. Un ujo de entrada de datos en modo texto acta como una fuente que proporciona una secuencia de caracteres (usualmente a travs de un buer de almacenamiento intermedio) desde el que se extraen los caracteres que representan a los datos de entrada, que posteriormente sern convertidos a la representacin interna adecuada.
Juan Lpez 12 3 1992

Lola Martnez 23 7 1987

Pepe Jimnez 17 8 1996

1 Si un chero binario es procesado por un programa que se ejecuta en un ordenador que utiliza una representacin interna distinta de la utilizada en el ordenador en que se cre podemos tener problemas de compatibilidad. 2 El carcter terminador de n de lnea no es visible, aunque se aprecian sus efectos al mostrarse los siguientes caracteres en la siguiente lnea.

Dpto. Lenguajes y Ciencias de la Computacin

Universidad de Mlaga

9.1. FLUJOS DE ENTRADA Y SALIDA ASOCIADOS A FICHEROS

105

Por el contrario, un ujo de salida de datos en modo texto acta como un sumidero que recibe una secuencia de caracteres (usualmente a travs de un buer de almacenamiento intermedio) al que se envan los caracteres que representan a los datos de salida, que previamente han sido convertidos al formato de texto adecuado.
(cin) PROGRAMA (cout)

Datos
Pepe 111 Maria 222 Juan 333 agenda.txt

Datos
Pepe 444 Maria 666 Juan 555 agenda.txt

Datos

(f_entrada)

(f_salida)

Datos

En el caso de entrada y salida a cheros, el lenguaje de programacin C++ posee mecanismos para asociar y vincular estos ujos con cheros almacenados en memoria secundaria en el sistema de cheros. As, toda la entrada y salida de informacin se realiza a travs de estos ujos vinculados a cheros, denominados manejadores de cheros. De este modo, una vez que un programa vincula un chero con un determinado ujo de entrada o salida, las operaciones de lectura o escritura funcionan como ya hemos estudiado en los ujos estndar cin y cout. Cuando un programa quiere realizar una entrada o salida de datos con un determinado chero, debe realizar las siguientes acciones:
1. Incluir la biblioteca <fstream>, que contiene los elementos necesarios para procesar el chero. 2. Usar el espacio de nombres std. 3. Declarar las variables que actuarn como manejadores de cheros. 4. Abrir el ujo de datos, vinculando la variable correspondiente con el chero especicado. Esta operacin establece un vnculo entre la variable (manejador de chero) denida en nuestro programa y el chero gestionado por el sistema operativo. De esta forma, toda transferencia de informacin entre el programa y un chero se realizar a travs de la variable manejador que ha sido vinculada con dicho chero. 5. Comprobar que la apertura del chero del paso previo se realiz correctamente. Si la vinculacin con el chero especicado no pudo realizarse por algn motivo (por ejemplo, si queremos hacer entrada de datos de un chero que no existe, o si es imposible crear un chero en el que escribir datos), entonces la operacin de apertura fallara. 6. Realizar la transferencia de informacin (de entrada o de salida) con el chero a travs de la variable de ujo vinculada al mismo. Dicha transferencia de informacin se puede realizar utilizado los mecanismos vistos en los captulos anteriores (3, 6.2.1). En el caso de salida, los datos debern escribirse siguiendo un formato adecuado que permita su posterior lectura. Por ejemplo, escribiendo separadores adecuados entre los diferentes valores almacenados. Normalmente, tanto la entrada como la salida de datos se realizan mediante un proceso iterativo. En el caso de entrada dicho proceso suele requerir la lectura de todo el contenido del chero. 7. Comprobar que el procesamiento del chero del paso previo se realiz correctamente. En el caso de procesamiento para entrada ello suele consistir en comprobar si el estado de la variable manejador indica que se ha alcanzado el nal del chero. En el procesamiento para salida suele consistir en comprobar si el estado de la variable manejador indica que se ha producido un error de escritura. 8. Finalmente, cerrar el ujo para liberar la variable manejador de su vinculacin con el chero. En caso de no cerrar el ujo, ste ser cerrado automticamente cuando termine el mbito de vida de la variable manejador del chero.

Nota: es importante tener en cuenta que cuando un ujo pasa al estado errneo (fail()), entonces cualquier operacin de entrada o salida que se realice sobre l tambin fallar. Las variables de tipo ujo pueden ser usadas como parmetros de subprogramas. En este caso hay que tener en cuenta que es necesario que, tanto si el chero se quiere pasar como un parmetro de entrada, salida o entrada/salida, dicho paso de parmetro debe hacerse por referencia (no constante).
Dpto. Lenguajes y Ciencias de la Computacin Universidad de Mlaga

106

CAPTULO 9. ALMACENAMIENTO EN MEMORIA SECUNDARIA: FICHEROS

9.2.

Entrada de Datos desde Ficheros de Texto

A continuacin, presentaremos con ms detalle y utilizando ejemplos concretos los pasos que se deben seguir para realizar la entrada de datos desde cheros de texto.
1. Debemos asegurarnos que hemos incluido biblioteca <fstream> y que utilizamos el espacio de nombres std. #include <fstream> using namespace std; 2. Necesitamos denir una variable que acte como manejador del chero del que queremos leer. Esta variable debe ser de tipo ifstream (input le stream ), disponible una vez que hemos importado la biblioteca fstream ifstream f_ent; 3. La variable f_ent ha sido declarada para ser asociada a un determinado ujo, pero aun no hemos procedido a realizar una vinculacin con un chero concreto. Al proceso de vincular una variable manejador de chero con un determinado chero se le conoce como abrir el chero. f_ent.open("fechas.txt"); En este ejemplo hemos utilizando una constante literal de tipo cadena de caracteres para asociar el manejador f_ent con el chero fechas.txt. En numerosas ocasiones el nombre del chero a abrir no es siempre el mismo, podemos tener programas en los que, por ejemplo, el nombre del chero a abrir sea uno concreto introducido por teclado. Para ello, podemos declarar la variable de tipo string correspondiente y utilizarla en el proceso de apertura. En tal caso debemos utilizar la funcin c_str() para adaptar la variable al tipo correcto antes de efectuar la llamada a open, como se muestra a continuacin. string nom_fich; cout << "Nombre del fichero a abrir: "; cin >> nom_fich; f_ent.open(nom_fich.c_str()); 4. Comprobar que la apertura del chero se realiz correctamente y evitar el procesamiento del chero en caso de fallo en la apertura. if (f_ent.fail()) { ... } 5. Realizar la entrada de datos con los operadores y subprogramas correspondientes, as como procesar la informacin leda. Por ejemplo, para un chero con el formato mostrado en la seccin 9, podramos leer dos cadenas con el nombre y apellidos de la persona (hasta el primer separador) y tres nmeros con el da, mes y ao de nacimiento de la persona3 . f_ent >> nombre >> apellidos >> dia >> mes >> anyo; o bien podramos estar interesados en leer una lnea completa en una variable de tipo string. string linea; getline(f_ent, linea); En el captulo 6.2.1 estudiamos que la lectura de datos puede requerir limpiar el buer de entrada en determinada situaciones. Ahora, aunque el ujo desde el que efectuar la entrada es un chero (en lugar de cin) el comportamiento es exactamente el mismo que estudiamos en dicho captulo. Por ese motivo, si utilizamos getline para leer una cadena del chero, es posible que necesitemos omitir (limpiar ) determinados caracteres del buer de entrada del chero para asegurarnos de que la lectura se realiza correctamente. En tal caso, utilizaramos el manipulador ws o bien la funcin ignore (sobre la variable manejador del chero) para limpiar el buer de entrada, al igual que hicimos en dicho captulo.
3 Suponemos

que cada uno de los datos ledos se encuentran seguidos de un separador en el chero. Universidad de Mlaga

Dpto. Lenguajes y Ciencias de la Computacin

9.2. ENTRADA DE DATOS DESDE FICHEROS DE TEXTO


f_ent >> edad ; f_ent.ignore(1000, \n) ; //--------------------------getline(f_ent, linea) ; f_ent >> edad ; //--------------------------f_ent >> ws ; getline(f_ent, linea) ;

107

Al igual que podemos usae la funcin get para leer un carcter de teclado, podemos utilizar dicha funcin para leer un carcter de un chero. En tal caso deberemos indicar el manejador del chero del que queremos realizar la lectura. char car; f_ent.get(car); Usualmente la lectura de datos de un chero se realiza dentro de un proceso iterativo que acaba cuando el chero no contiene datos que procesar. Necesitamos algn modo de detectar que se ha alcanzado el n de un chero durante un proceso de lectura. Para ello, comprobaremos si se ha producido un fallo al intentar efectuar una operacin de lectura. Por lo general, este proceso iterativo suele responder al siguiente esquema general: Lectura de datos Si la lectura no ha sido correcta, entonces terminar el proceso iterativo. En otro caso, realizamos el procesamiento de los datos ledos, y continuamos el proceso iterativo, leyendo nuevos datos ... f_ent >> datos; while (! f_ent.fail() ... ) { procesar(datos, ...); f_ent >> datos; } 6. Comprobar que el procesamiento del chero se realiz correctamente, es decir, que el chero se ley completamente hasta el nal de mismo (eof representa end-of-le ). Si el chero no acab correctamente es porque se produjo un error de lectura durante su procesamiento. En tal caso, posiblemente nos interesar tratar adecuadamente dicha situacin de error. if (!f_ent.fail() || f_ent.eof()) { /* OK */ } 7. Finalmente, cerrar el ujo liberando la variable de su vinculacin. f_ent.close();

Acabaremos esta seccin mostrando un ejemplo completo en el que utilizamos todos los elementos introducidos anteriormente. Queremos leer nmeros4 de un chero y mostrarlos en pantalla. En numerosas ocasiones el esquema del programa mostrado a continuacin puede ser utilizado en otros programas. Para ello bastara con modicar el subprograma leer, adaptando la lectura al caso concreto que deseemos tratar, y el subprograma procesar para el procesamiento que deseemos realizar con los datos ledos.
#include <iostream> #include <fstream> #include <string> using namespace std; enum Codigo { OK, ERROR_APERTURA, ERROR_FORMATO }; void procesar(int num) { cout << num << endl; } void leer(ifstream& fich, int& num)
4 Se

supone que los nmeros estn separados por espacios o separadores adecuados. Universidad de Mlaga

Dpto. Lenguajes y Ciencias de la Computacin

108
{

CAPTULO 9. ALMACENAMIENTO EN MEMORIA SECUNDARIA: FICHEROS

fich >> num; } void leer_fich(const string& nombre_fichero, Codigo& ok) { ifstream f_ent; f_ent.open(nombre_fichero.c_str()); if (f_ent.fail()) { ok = ERROR_APERTURA; } else { int numero; leer(f_ent, numero); while (! f_ent.fail()) { procesar(numero); leer(f_ent, numero); } if (!f_ent.fail() || f_ent.eof()) { ok = OK; } else { ok = ERROR_FORMATO; } f_ent.close(); } } void codigo_error(Codigo ok) { switch (ok) { case OK: cout << "Fichero procesado correctamente" << endl; break; case ERROR_APERTURA: cout << "Error en la apertura del fichero" << endl; break; case ERROR_FORMATO: cout << "Error de formato en la lectura del fichero" << endl; break; } } int main() { Codigo ok; string nombre_fichero; cout << "Introduzca el nombre del fichero: "; cin >> nombre_fichero; leer_fich(nombre_fichero, ok); codigo_error(ok); }

9.3.

Salida de Datos a Ficheros de Texto

Para escribir datos en cheros de texto utilizamos los mismos elementos que para realizar la lectura, pero considerando las siguientes diferencias:
1. Debemos asegurarnos que hemos incluido biblioteca <fstream> y que utilizamos el espacio de nombres std. #include <fstream> using namespace std;
Dpto. Lenguajes y Ciencias de la Computacin Universidad de Mlaga

9.3. SALIDA DE DATOS A FICHEROS DE TEXTO

109

2. Necesitamos denir una variable que acte como manejador del chero al que queremos escribir. Esta variable debe ser de tipo ofstream (output le stream ), disponible una vez que hemos importado la biblioteca fstream ofstream f_sal; 3. La variable f_sal ha sido declarada para ser asociada a un determinado ujo, pero aun no hemos procedido a realizar una vinculacin con un chero concreto. Al proceso de vincular una variable manejador de chero con un determinado chero se le conoce como abrir el chero. f_sal.open("fechas.txt"); En este ejemplo hemos utilizando una constante literal de tipo cadena de caracteres para asociar el manejador f_sal con el chero fechas.txt. En numerosas ocasiones el nombre del chero a abrir no es siempre el mismo, podemos tener programas en los que, por ejemplo, el nombre del chero a abrir sea uno concreto introducido por teclado. Para ello, podemos declarar la variable de tipo string correspondiente y utilizarla en el proceso de apertura. En tal caso debemos utilizar la funcin c_str() para adaptar la variable al tipo correcto antes de efectuar la llamada a open, como se muestra a continuacin. string nom_fich; cout << "Nombre del fichero a abrir: "; cin >> nom_fich; f_sal.open(nom_fich.c_str()); 4. Comprobar que la apertura del chero se realiz correctamente y evitar el procesamiento del chero en caso de fallo en la apertura. if (f_sal.fail()) { ... } 5. Si escribimos datos en un chero es con la intencin de proceder en un futuro a su lectura. As pues, debemos escribir los datos con los separadores adecuados entre ellos, permitiendo as que futuras lecturas del mismo funcionen correctamente. Por ejemplo, si queremos escribir datos de una persona en un chero como el usado en la seccin anterior, ser necesario escribir los separadores entre cada dos valores. Por ejemplo, a continuacin utilizamos espacios en blanco entre cada dos datos de una persona y un salto de lnea para separar una persona de la siguiente. f_sal << nombre << " " << apellidos << " " << dia << " " << mes << " " << anyo << endl; Usualmente la escritura de datos se realiza mediante un proceso iterativo que naliza cuando se escriben en el chero todos los datos apropiados y mientras el estado del ujo sea correcto. while ( ... !f_sal.fail() ) { ... } 6. Al acabar el bucle es conveniente comprobar si se ha producido algn error durante la escritura en el chero (el ujo est en un estado incorrecto) y en su caso, procesarlo adecuadamente. if (!f_sal.fail()) { /* OK */ } 7. Finalmente, cerrar el ujo liberando la variable de su vinculacin. f_sal.close();

Por ejemplo, a continuacin se muestra un programa que lee nmeros de teclado (hasta introducir un cero) y los escribe en un chero de texto. Los nmeros se escriben separados por un carcter de n de lnea (endl), lo que permite que el chero aparezca con cada nmero en una lnea diferente si es mostrado por un editor o que los nmeros puedan ser ledos y procesados por un programa como el presentado en el ejemplo anterior.
Dpto. Lenguajes y Ciencias de la Computacin Universidad de Mlaga

110

CAPTULO 9. ALMACENAMIENTO EN MEMORIA SECUNDARIA: FICHEROS


#include <iostream> #include <fstream> #include <string> using namespace std; enum Codigo { OK, ERROR_APERTURA, ERROR_FORMATO }; void escribir(ofstream& f_sal, int num) { f_sal << num << endl ; } void escribir_fich(const string& nombre_fichero, Codigo& ok) { ofstream f_sal; f_sal.open(nombre_fichero.c_str()); if (f_sal.fail()) { ok = ERROR_APERTURA; } else { int numero; cin >> numero; while ((numero != 0) && ! cin.fail() && ! f_sal.fail()) { escribir(f_sal, numero); cin >> numero; } if (!f_sal.fail()) { ok = OK; } else { ok = ERROR_FORMATO; } f_sal.close(); } }

Hemos utilizado un bucle de escritura que acaba cuando se introduce un cero por teclado. Estamos acostumbrados a resolver problemas como ste, en los que trabajamos con secuencias de nmeros que se leen de teclado hasta que se cumple una cierta condicin. En este caso hemos incluido dos condiciones adicionales en la expresin que controla el n del bucle (! cin.fail() && ! f_sal.fail()). Con la primera condicin (! cin.fail()) nos aseguramos de que el proceso iterativo se detiene si se produce algn error durante la lectura de teclado y el ujo cin entra en un estado incorrecto. Hasta ahora siempre hemos supuesto que el usuario introduce datos correctos, por lo que no hemos controlado posibles errores en la entrada. Sin embargo, esta suposicin puede resultar peligrosa en un programa como el mostrado en este ejemplo porque, en caso de ocurrir algun error de lectura (por ejemplo, si en lugar de introducir un nmero de tipo int el usuario introduce una cadena de caracteres), el ujo de entrada cin entrara en un estado de error, dando lugar a un bucle innito que podra hacer que el chero creciera sin control hasta ocupar todo el espacio disponible en el dispositivo que almacena el chero. La inclusin de esta condicin evita este riesgo. Con la segunda condicin (! f_sal.fail()) nos aseguramos de que el proceso iterativo se detiene si se produce algn error durante la escritura en el chero, por ejemplo, si el dispositivo no dispone de memoria suciente.
void codigo_error(Codigo ok) { switch (ok) { case OK: cout << "Fichero guardado correctamente" << endl; break; case ERROR_APERTURA: cout << "Error en la apertura del fichero" << endl;
Dpto. Lenguajes y Ciencias de la Computacin Universidad de Mlaga

9.4. EJEMPLOS
break; case ERROR_FORMATO: cout << "Error de formato al escribir al fichero" << endl; break; } } int main() { Codigo ok; string nombre_fichero; cout << "Introduzca el nombre del fichero: "; cin >> nombre_fichero; escribir_fich(nombre_fichero, ok); codigo_error(ok); }

111

9.4.

Ejemplos

Ejemplo 1. Copia del contenido de un chero en otro. A continuacin, mostramos un programa que lee carcter a carcter el contenido de un chero de texto y crea un nuevo chero con el mismo contenido. El programa se basa en el subprograma copiar_fichero, que recibe dos cadenas de caracteres con el nombre de los cheros origen y destino, realiza la copia y devuelve el estado de error resultante de efectuar la operacin.
#include <iostream> #include <fstream> #include <string> using namespace std; enum Codigo { OK, ERROR_APERTURA_ENT, ERROR_APERTURA_SAL, ERROR_FORMATO }; void copiar_fichero(const string& salida, const string& entrada, Codigo& ok) { ifstream f_ent; f_ent.open(entrada.c_str()); if (f_ent.fail()) { ok = ERROR_APERTURA_ENT; } else { ofstream f_sal; f_sal.open(salida.c_str()); if (f_sal.fail()) { ok = ERROR_APERTURA_SAL; } else { char ch; f_ent.get(ch); while (! f_ent.fail() && ! f_sal.fail()) { f_sal << ch; f_ent.get(ch); } if ((! f_ent.fail() || f_ent.eof()) && ! f_sal.fail()) { ok = OK; } else { ok = ERROR_FORMATO; } f_sal.close(); } f_ent.close();
Dpto. Lenguajes y Ciencias de la Computacin Universidad de Mlaga

112
} }

CAPTULO 9. ALMACENAMIENTO EN MEMORIA SECUNDARIA: FICHEROS

En este ejemplo trabajamos con dos cheros simultneamente, uno para entrada y otro para salida, por lo que usamos dos manejadores diferentes. Al igual que en los ejemplos anteriores, antes de trabajar con cada chero hay que asociar su nombre con el manejador correspondiente, posteriormente se efectan las operaciones que corresponda con cada uno y, nalmente, se liberan las vinculaciones de los manejadores con los cheros5 . En este caso el procesamiento del chero de entrada en cada paso consiste en leer un carcter (usamos get porque queremos leer tambin los separadores) y el procesamiento del chero de salida consiste en escribir un nico carcter.
void codigo_error(Codigo ok) { switch (ok) { case OK: cout << "Fichero procesado correctamente" << endl; break; case ERROR_APERTURA_ENT: cout << "Error en la apertura del fichero de entrada" << endl; break; case ERROR_APERTURA_SAL: cout << "Error en la apertura del fichero de salida" << endl; break; case ERROR_FORMATO: cout << "Error de formato en la lectura del fichero" << endl; break; } } int main() { Codigo ok; string entrada, salida; cout << "Introduzca el nombre del fichero de entrada: "; cin >> entrada; cout << "Introduzca el nombre del fichero de salida: "; cin >> salida; copiar_fichero(salida, entrada, ok); codigo_error(ok); }

Ejemplo 2 Ejemplo de un programa que crea, guarda y carga una agenda personal.
//------------------------------------------------------------------------#include <iostream> #include <fstream> #include <string> #include <array> #include <cctype> using namespace std ; //------------------------------------------------------------------------struct Fecha { unsigned dia ; unsigned mes ;
5 Los manejadores de cheros son en este caso variables locales, que se destruyen automticamente al acabar el subprograma, liberando los recursos necesarios. As pues, en este caso podramos haber omitido la liberacin explcita (close). Sin embargo, optamos por incluirla porque ello reeja claramente el proceso a seguir en cualquier manipulacin de cheros (abrir, usar, cerrar)

Dpto. Lenguajes y Ciencias de la Computacin

Universidad de Mlaga

9.4. EJEMPLOS
unsigned anyo ; } ; struct Persona { string nombre ; string tfn ; Fecha fnac ; } ; const int MAX = 100 ; typedef array<Persona, MAX> APers ; struct Agenda { int nelms ; APers elm ; } ; //------------------------------------------------------------------------void inic_agenda(Agenda& ag) { ag.nelms = 0 ; } void anyadir_persona(Agenda& ag, const Persona& p, bool& ok) { if (ag.nelms < int(ag.elm.size())) { ag.elm[ag.nelms] = p ; ++ag.nelms ; ok = true ; } else { ok = false ; } } //------------------------------------------------------------------------void leer_fecha(Fecha& f) { cout << "Introduza fecha de nacimiento (dia mes ao): " ; cin >> f.dia >> f.mes >> f.anyo ; } void leer_persona(Persona& p) { cout << "Introduza nombre: " ; cin >> ws ; getline(cin, p.nombre) ; cout << "Introduza telfono: " ; cin >> p.tfn ; leer_fecha(p.fnac) ; } void nueva_persona(Agenda& ag) { bool ok ; Persona p ; leer_persona(p) ; if (! cin.fail()) { anyadir_persona(ag, p, ok) ; if (!ok) { cout << "Error al introducir la nueva persona" << endl ; } } else { cout << "Error al leer los datos de la nueva persona" << endl ; cin.clear() ; cin.ignore(1000, \n) ; } }

113

Dpto. Lenguajes y Ciencias de la Computacin

Universidad de Mlaga

114

CAPTULO 9. ALMACENAMIENTO EN MEMORIA SECUNDARIA: FICHEROS


//------------------------------------------------------------------------void escribir_fecha(const Fecha& f) { cout << f.dia << / << f.mes << / << f.anyo ; } void escribir_persona(const Persona& p) { cout << "Nombre: " << p.nombre << endl ; cout << "Telfono: " << p.tfn << endl ; cout << "Fecha nac: " ; escribir_fecha(p.fnac) ; cout << endl ; } void escribir_agenda(const Agenda& ag) { for (int i = 0 ; i < ag.nelms ; ++i) { cout << "----------------------------------------" << endl ; escribir_persona(ag.elm[i]) ; } cout << "----------------------------------------" << endl ; } //------------------------------------------------------------------------// FORMATO DEL FICHERO DE ENTRADA: // // <nombre> <RC> // <telfono> <dia> <mes> <ao> <RC> // <nombre> <RC> // <telfono> <dia> <mes> <ao> <RC> // ... //------------------------------------------------------------------------void leer_fecha(ifstream& fich, Fecha& f) { fich >> f.dia >> f.mes >> f.anyo ; } void leer_persona(ifstream& fich, Persona& p) { fich >> ws ; getline(fich, p.nombre) ; fich >> p.tfn ; leer_fecha(fich, p.fnac) ; } //---------------------------------------------// Otra posible implementacin // void leer_persona(ifstream& fich, Persona& p) // { // getline(fich, p.nombre) ; // fich >> p.tfn ; // leer_fecha(fich, p.fnac) ; // fich.ignore(1000, \n) ; // } //---------------------------------------------void leer_agenda(const string& nombre_fich, Agenda& ag, bool& ok) { ifstream fich ; Persona p ; fich.open(nombre_fich.c_str()) ; if (fich.fail()) { ok = false ;

Dpto. Lenguajes y Ciencias de la Computacin

Universidad de Mlaga

9.4. EJEMPLOS
} else { ok = true ; inic_agenda(ag) ; leer_persona(fich, p) ; while (!fich.fail() && ok) { anyadir_persona(ag, p, ok) ; leer_persona(fich, p) ; } ok = ok && (!fich.fail() || fich.eof()) ; fich.close() ; } } void cargar_agenda(Agenda& ag) { bool ok ; string nombre_fich ; cout << "Introduce el nombre del fichero: " ; cin >> nombre_fich ; leer_agenda(nombre_fich, ag, ok) ; if (!ok) { cout << "Error al cargar el fichero" << endl ; } } //------------------------------------------------------------------------// FORMATO DEL FICHERO DE SALIDA: // // <nombre> <RC> // <telfono> <dia> <mes> <ao> <RC> // <nombre> <RC> // <telfono> <dia> <mes> <ao> <RC> // ... //------------------------------------------------------------------------void escribir_fecha(ofstream& fich, const Fecha& f) { fich << f.dia << << f.mes << << f.anyo ; } void escribir_persona(ofstream& fich, const Persona& p) { fich << p.nombre << endl ; fich << p.tfn << ; escribir_fecha(fich, p.fnac) ; fich << endl ; } void escribir_agenda(const string& nombre_fich, const Agenda& ag, bool& ok) { ofstream fich ; fich.open(nombre_fich.c_str()) ; if (fich.fail()) { ok = false ; } else { int i = 0 ; while ((i < ag.nelms) && (! fich.fail())) { escribir_persona(fich, ag.elm[i]) ; ++i ; } ok = ! fich.fail() ; fich.close() ; }

115

Dpto. Lenguajes y Ciencias de la Computacin

Universidad de Mlaga

116

CAPTULO 9. ALMACENAMIENTO EN MEMORIA SECUNDARIA: FICHEROS


} void guardar_agenda(const Agenda& ag) { bool ok ; string nombre_fich ; cout << "Introduce el nombre del fichero: " ; cin >> nombre_fich ; escribir_agenda(nombre_fich, ag, ok) ; if (!ok) { cout << "Error al guardar el fichero" << endl ; } } //------------------------------------------------------------------------char menu() { char op ; cout << endl ; cout << "C. Cargar Agenda" << endl ; cout << "M. Mostrar Agenda" << endl ; cout << "N. Nueva Persona" << endl ; cout << "G. Guardar Agenda" << endl ; cout << "X. Fin" << endl ; do { cout << endl << " Opcin: " ; cin >> op ; op = char(toupper(op)) ; } while (!((op == C)||(op == M)||(op == N)||(op == G)||(op == X))) ; cout << endl ; return op ; } //------------------------------------------------------------------------int main() { Agenda ag ; char op ; inic_agenda(ag) ; do { op = menu() ; switch (op) { case C: cargar_agenda(ag) ; break ; case M: escribir_agenda(ag) ; break ; case N: nueva_persona(ag) ; break ; case G: guardar_agenda(ag) ; break ; } } while (op != X) ; } //-------------------------------------------------------------------------

Dpto. Lenguajes y Ciencias de la Computacin

Universidad de Mlaga

Captulo 10

Mdulos y Bibliotecas
Cuando se desarrollan programas de complejidad media/alta, el cdigo fuente no suele encontrarse en un nico chero, sino distribuido entre varios mdulos. En este contexto, un mdulo es una entidad que agrupa diferentes elementos (subprogramas, tipos, constantes) interrelacionados entre s. El programa completo se forma mediante la composicin de diferentes mdulos y, a su vez, un mdulo puede incluir a otros si necesita hacer uso de los elementos denidos en stos. La estructuracin de un programa en mdulos presenta numerosas ventajas: Permite aumentar la localidad y cohesin del cdigo, aislndolo del exterior. Es decir, permite separar y aislar el cdigo encargado de resolver un determinado problema. Facilita la compilacin separada. Si se necesita modicar algn elemento interno a un mdulo, no ser necesario volver a compilar todo el programa sino nicamente la parte afectada por dicho cambio. Ello es especialmente importante en programas grandes, en los que el tiempo de compilacin puede ser considerable. Facilita la reutilizacin del cdigo. Es posible, tanto utilizar las bibliotecas del sistema como crear mdulos con nuevas bibliotecas que puedan ser reutilizadas por mltiples programas. Esta distribucin de bibliotecas se puede hacer en cdigo objeto, por lo que no es necesario distribuir el cdigo fuente de la misma.

M1 GUI Mem Math File

Main

M2 MP3 JPG Sckts

Programa Bbl. Utilidades API S.O. S.O.

I/O

Proc

Kernel S.O.

En la gura se muestra un determinado programa que resuelve un determinado problema, cuya solucin principal se ha dividido en varios mdulos de programa (Main, M1 y M2). Adems, se hace uso de varios mdulos de biblioteca que proporcionan utilidades grcas, matemticas y de tratamiento de imgenes; otros mdulos de biblioteca son proporcionados por el sistema operativo y dan acceso a servicios de entrada/salida y comunicaciones por Internet.

10.1.

Interfaz e Implementacin del Mdulo

En el lenguaje de programacin C++, normalmente un mdulo se compone de dos cheros: uno donde aparece el cdigo que resuelve un determinado problema o conjunto de problemas (la parte 117

118

CAPTULO 10. MDULOS Y BIBLIOTECAS

privada), y otro que contiene las deniciones de tipos, constantes y prototipos de subprogramas que el mdulo ofrece (la parte pblica). Hablaremos de la implementacin del mdulo cuando nos reramos al chero que contiene su parte privada, y usaremos el tmino interfaz del mdulo para referirmos al chero que contiene su parte pblica1 . Normalmente un programa completo se compone de varios mdulos, cada uno con su chero de encabezamiento (interfaz) y con su chero de de implementacin, y de un mdulo principal donde reside la funcin principal main. Los cheros de implementacin tendrn una extensin .cpp (tambin suelen utilizarse otras extensiones como .cxx y .cc) y los cheros de encabezamiento tendrn una extensin .hpp (tambin suelen utilizarse otras extensiones como .hxx, .hh y .h). Cuando en un determinado mdulo se desee hacer uso de las utilidades proporcionadas por otro mdulo, ste deber incluir el chero de encabezamiento (interfaz) del mdulo que se vaya a utilizar. Adems, el chero de implementacin de un determinado mdulo deber incluir al chero de encabezamiento de su propio mdulo. Por ejemplo, si quisiramos obtener un programa en el que se trabaje con nmeros complejos, podramos pensar en disponer de un mdulo que se encargue de denir el tipo Complejo junto a una serie de operaciones. En el chero de interfaz (complejo.hpp ) se encontrara la denicin del tipo y la declaracin de los subprogramas y en el chero de implementacin (complejo.cpp ) se encontrara la implementacin de los subprogramas. El mdulo principal debera importar el chero de interfaz (complejo.hpp ) para poder hacer uso del tipo Complejo y de los subprogramas. Adems, el mdulo de implementacin tambin debera importar a su chero de interfaz para poder conocer aquello que se desea implementar. El siguiente dibujo muestra el esquema bsico de los cheros usados en este ejemplo, donde se utiliza la directiva #include para incluir los cheros de encabezamiento que corresponda:

main.cpp (Principal) #include "complejo.hpp" // utilizacin de complejo int main() { }

complejo.hpp (Interfaz) #ifndef _complejo_hpp_ #define _complejo_hpp_ // interfaz de complejo // pblico #endif

complejo.cpp (Implementacin) #include "complejo.hpp" // implementacin de complejo // privado

La inclusin de cheros de encabezamiento en nuestros mdulos de programa no es algo nuevo para nosotros. Estamos acostumbrados a hacerlo cuando necesitamos efectuar entrada/salida con la consola (<iostream>), trabajar con cadenas de caracteres (<string>) o con con cheros (<fstream>). En estos casos omitimos la extensin del chero de encabezamiento y usamos <...> para delimitar el nombre del chero a incluir. Lo hemos hecho de esta forma porque se trata de bibliotecas estndares, de uso muy frecuente e instaladas en una localizacin tpica del sistema de archivos. Cuando el compilador detecta que el nombre del chero a incluir est delimitado por <...>, sabe que se trata de una de tales bibliotecas y dnde localizar el chero correspondiente. En nuestro ejemplo, el mdulo para trabajar con nmeros complejos no est instalado con el resto de bibliotecas estndares, sino que sus cheros de encabezamiento e implementacin se encuentran en el directorio de trabajo, por lo que usamos "..." para delimitar el chero a incluir, indicando as al compilador dnde localizar los cheros necesarios. Guardas en un Fichero de Encabezamiento Las deniciones en los cheros de encabezamiento (interfaz) sern especicadas entre las guardas (directivas de compilacin condicional) para evitar la inclusin duplicada de las deniciones all contenidas. El nombre de la guarda usualmente se deriva del nombre del chero, como se indica en el siguiente ejemplo donde el mdulo complejo tendr los siguientes cheros de encabezamiento y de implementacin (en determinadas circunstancias, puede ser conveniente que al nombre de la guarda se le aada tambin el nombre del espacio de nombres que se explicar en la siguiente seccin):
1A

este chero tambin se le denomina chero de encabezamiento o chero de cabecera (header le en ingls). Universidad de Mlaga

Dpto. Lenguajes y Ciencias de la Computacin

10.2. COMPILACIN SEPARADA Y ENLAZADO

119

Fichero: complejo.hpp (Interfaz)


// Guarda para evitar inclusin duplicada #ifndef _complejo_hpp_ #define _complejo_hpp_ // Definiciones Pblicas de: // * Constantes // * Tipos (Enum, Registros, Clases) // * Prototipos de Subprogramas #endif // Fin de guarda

Fichero: complejo.cpp (Implementacin)


#include "complejo.hpp"

// Implementaciones Privadas de: // * Constantes Privadas // * Tipos Privados // * Subprogramas // * Clases

Directrices para el Diseo de Ficheros de Encabezamiento Con objeto de organizar adecuadamente el diseo de los cheros de encabezamiento de los mdulos, tendremos en cuenta las siguientes directrices:

Un chero de encabezamiento slo deber contener deniciones de constantes, deniciones de tipos y prototipos de los subprogramas que exporta (parte pblica) el mdulo. No deber contener deniciones de variables globales, ni la implementacin de cdigo (de subprogramas y mtodos). En este ltimo caso se contemplan algunas excepciones, tales como la denicin de subprogramas simples en lnea (vase 5.6) y la denicin de subprogramas y clases genricas. (vase 12) .

El mecanismo de inclusin de cheros de encabezamiento debe ser robusto ante posibles inclusiones duplicadas. Para ello, siempre se utilizar el mecanismo de guardas explicado anteriormente.

Un chero de encabezamiento debe incluir todos los cheros de encabezamiento de otros mdulos que necesite para su propia denicin. De esta forma, no importar el orden de inclusin de los cheros de encabezamiento, ya que cada chero contiene todo lo necesario para su compilacin.

10.2.

Compilacin Separada y Enlazado

El diseo de un programa mediante su organizacin en mdulos permite compilar de forma separada los diferentes mdulos que lo componen. Ello contribuye a que el proceso de compilacin sea ms exible y rpido. Es frecuente que estemos trabajando con un programa en el que pretendemos introducir algn cambio o corregir algn error. En este caso, posiblemente habremos compilado el programa previamente y dispongamos del cdigo objeto de la versin anterior. Si las modicaciones realizadas nicamente afectan a un determinado mdulo, no es necesario volver a compilar el cdigo fuente del resto de mdulos. Bastar con compilar el cdigo fuente del mdulo afectado y enlazar el cdigo objeto obtenido con el cdigo objeto del resto de los mdulos del programa.

Dpto. Lenguajes y Ciencias de la Computacin

Universidad de Mlaga

120
complejo.hpp
Interfaz Pblico C.Fuente C++

CAPTULO 10. MDULOS Y BIBLIOTECAS


mat.hpp
Interfaz + Implementacin Pblico C.Fuente C++

iostream
Interfaz Pblico C.Fuente C++

string
Interfaz Pblico C.Fuente C++

complejo.cpp
Implementacin Privado C.Fuente C++

main.cpp
Programa Principal C.Fuente C++

bblstd.cpp
Implementacin Privado C.Fuente C++

Compilacin

Compilacin

Compilacin

complejo.o
Implementacin Pblico C.Objeto

main.o
Programa Principal C.Objeto

bblstd.o
Implementacin Pblico C.Objeto

Enlazado main
Programa Completo C.Ejecutable

Para compilar un mdulo de forma independiente, nicamente hay que con compilar su chero de implementacin, por ejemplo complejo.cpp. El resultado es un chero en cdigo objeto, por ejemplo complejo.o. El compilador compila un cdigo fuente que ser el resultado de incluir en el chero de implementacin el contenido de los cheros de encabezamiento, como se indica en la gura anterior. En cualquier caso, una vez que tenemos los cheros objetos de los diferentes mdulos, se procede a su enlazado nal, obteniendo el chero ejecutable. Dependiendo del entorno en el que trabajemos, la tarea de compilacin podr ser realizada, bien directamente desde la lnea de comando o con herramientas adecuadas en un entorno de desarrollo integrado. A continuacin mostramos algunas de las opciones que se pueden realizar, suponiendo que trabajamos directamente desde la lnea de comando y que utilizamos el compilador GNU GCC. Podramos generar los cheros objetos de los dos mdulos de nuestro programa (complejo.o y main.o) de la siguiente forma:
g++ -ansi -Wall -Werror -c complejo.cpp g++ -ansi -Wall -Werror -c main.cpp

y enlazar los cdigos objeto generados en el punto anterior para generar el chero ejecutable main, con el siguiente comando:
g++ -ansi -Wall -Werror -o main main.o complejo.o

Hay otras posibilidades, tambin es posible realizar la compilacin y enlazado en el mismo comando:
g++ -ansi -Wall -Werror -o main main.cpp complejo.cpp

o incluso mezclar compilacin de cdigo fuente y enlazado de cdigo objeto:


g++ -ansi -Wall -Werror -o main main.cpp complejo.o

Hay que tener en cuenta que el compilador enlaza automticamente el cdigo generado con las bibliotecas estndares de C++ y, por lo tanto, no es necesario que stas se especiquen explcitamente. Sin embargo, en caso de ser necesario, tambin es posible especicar el enlazado con bibliotecas externas:
g++ -ansi -Wall -Werror -o main main.cpp complejo.cpp -ljpeg

Estas bibliotecas no son ms que una agregacin de mdulos compilados a cdigo objeto, y organizadas adecuadamente para que puedan ser reutilizados por muy diversos programas.

10.3.

Espacios de Nombres

Cuando se trabaja con mltiples mdulos y bibliotecas, es posible que se produzcan colisiones en los identicadores utilizados para nombrar a diferentes entidades. Es posible que un mismo
Dpto. Lenguajes y Ciencias de la Computacin Universidad de Mlaga

10.3. ESPACIOS DE NOMBRES

121

identicador sea utilizado en un mdulo o biblioteca para nombrar a una determinada entidad y en otro mdulo diferente se utilice para nombrar a otra entidad diferente. Por ejemplo, podramos tener una biblioteca que dena en su interfaz una constante llamada MAX, con valor 100, y otra biblioteca diferente que utilice el mismo identicador para denir una constante con un sentido y valor diferente. Si nuestro programa quiere hacer uso de ambas bibliotecas, a cual de ellas nos referimos cuando usemos la constante MAX?. Necesitamos algn mecanismo para identicar exactamente a qu entidad nos referimos en cada caso. El lenguaje de programacin C++ permite solucionar este tipo de situaciones ambiguas mediante el uso de espacios de nombres (namespace en ingls), que permiten agrupar bajo una misma denominacin (jerarqua) un conjunto de declaraciones y deniciones, de tal forma que dicha denominacin ser necesaria para identicar y diferenciar cada entidad declarada. En nuestro ejemplo, una de las constantes MAX sera denida en el mbito de un espacio de nombres concreto y la otra en el mbito de otro espacio de nombres diferente. Posteriormente podramos referirnos a cada una de ellas indicando el identicador y el espacio de nombres en el que est denida, lo que evita cualquier posibilidad de ambigedad. Para denir un espacio de nombres se utiliza la palabra reservada namespace seguida por el identicador del espacio de nombres, y entre llaves las declaraciones y deniciones que deban estar bajo dicha jerarqua del espacio de nombres. Los espacios de nombres pueden ser nicos para un determinado mdulo o, por el contrario, pueden extenderse a mltiples mdulos y bibliotecas gestionados por el mismo proveedor. Por ejemplo, todas las entidades denidas en la biblioteca estndar se encuentran bajo el espacio de nombres std. El identicador del espacio de nombres puede ser derivado del propio nombre del chero, puede incluir una denominacin relativa al proveedor del mdulo, o alguna otra denominacin ms compleja que garantice que no habr colisiones en el identicador del espacio de nombres. Por ejemplo, podemos denir el mdulo complejo dentro del espacio de nombres umalcc, que hara referencia a un proveedor del departamento de Lenguajes y Ciencias de la Computacin de la Universidad de Mlaga.
main.cpp (Principal) #include <iostream> #include "complejo.hpp" using namespace std ; using namespace umalcc ; // utilizacin de complejo int main() { } complejo.hpp (Interfaz) #ifndef _complejo_hpp_ #define _complejo_hpp_ #include <...otros...> // interfaz de complejo namespace umalcc { } #endif complejo.cpp (Implementacin) #include "complejo.hpp" #include <...otros...> // implementacin de complejo namespace umalcc { }

Ntese que la inclusin de cheros de encabezamiento se debe realizar externamente a la denicin de los espacios de nombres. Utilizacin de Espacios de Nombres Una vez que las entidades han sido denidas dentro de un espacio de nombres, stas no pueden ser utilizadas directamente, sino que es necesario algn tipo de calicacin que permita referenciar e identicar a las entidades dentro de los espacios de nombres en los que han sido denidas. Ello se puede realizar de varias formas, dependiendo de las circunstancias donde se produzca esta utilizacin: Todos los identicadores denidos dentro de un espacio de nombres determinado son visibles y accesibles directamente desde dentro del mismo espacio de nombres, sin necesidad de calicacin. Fuera del espacio de nombres, el uso de un identicador denido dentro de un espacio de nombres debe especicar claramente el espacio de nombres al que pertenece. Para ello se puede utilizar una calicacin explcita, indicando el identicador que designa al espacio de
Dpto. Lenguajes y Ciencias de la Computacin Universidad de Mlaga

122

CAPTULO 10. MDULOS Y BIBLIOTECAS nombres, seguido del operador :: y del identicador de la entidad a la que queremos aludir. Por ejemplo, para aludir a un identicador MAX, denido en el espacio de nombres umalcc, habra que utilizar umalcc::MAX. En el caso de cheros de implementacin (con extensin .cpp) podemos estar interesados en utilizar con frecuencia identicadores de un mismo espacio de nombres. En estos casos resultara ms cmodo poder omitir la calicacin explcita. En estos casos resulta ms cmodo suponer que, por defecto, nos referimos a un determinado espacio de nombres. Para ello se utiliza la directiva using namespace, que pone disponibles (accesibles) todos los identicadores de dicho espacio de nombres completo, que podrn ser accedidos directamente, sin necesidad de calicacin explcita. Esto no resulta nuevo para nosotros, ya que estamos acostumbrados a utilizar dicha directiva para hacer accesible el espacio de nombres std
using namespace std ;

Por ejemplo, como las bibliotecas estndar (iostream, string o fstream) se denen en el espacio de nombres std, para usar alguna de sus entidades (por ejemplo, el tipo string), tenemos dos posibilidades: utilizar una calicacin explcita como se indica en el punto anterior (std::string) o, como hemos hecho hasta ahora en este curso, usar la directiva using namespace y suponer que los identicadores estn denidos en el espacio de nombres std. El uso de la directiva using namespace no est recomendado en los cheros de encabezamiento, donde siempre utilizaremos calicacin explcita. Por ejemplo, si queremos crear un mdulo con un chero de encabezamiento personas.hpp, optaramos por calicar explcitamente los identicadores cuando fuera necesario:
namespace umalcc { struct Persona { std::string nombre ; int edad ; } ; typedef std::array<int, 20> Vector ; void leer(std::string& nombre) ; }

Es posible que se utilice la directiva using namespace para hacer accesible a varios espacios de nombres simultneamente. En tal caso existe la posibilidad de que volvamos a tener problemas de colisin entre los identicadores. Puede haber varios identicadores que denen entidades diferentes y que son accesibles en dos espacios de nombres diferentes. Ello no supone ningn problema si nuestro programa no utiliza los identicadores en conicto. Sin embargo, en caso de utilizarlos el compilador no podra saber a qu entidad nos estamos reriendo. Para solucionar este tipo de conictos el programador debe utilizar la calicacin explcita con este identicador, eliminando as la ambigedad en su utilizacin.
main.cpp #include <iostream> #include <string> #include "datos.hpp" using namespace std ; using namespace umalcc ; // utilizacin de datos int main() { string colision ; std::string nombre_1 ; umalcc::string nombre_2 ; } datos.hpp #ifndef _datos_hpp_ #define _datos_hpp_ #include <array> #include <...otros...> // interfaz de datos namespace umalcc { struct string { std::array<char, 50> datos ; int size ; } ; } #endif datos.cpp #include "datos.hpp" #include <...otros...> namespace umalcc { }

Es importante remarcar que no es adecuado utilizar la directiva using namespace dentro de cheros de encabezamiento. Ello es as porque estos cheros estn pensados para ser incluidos por
Dpto. Lenguajes y Ciencias de la Computacin Universidad de Mlaga

10.3. ESPACIOS DE NOMBRES

123

otros mdulos, por lo que se pondran disponibles (accesibles) todos los identicadores de dicho espacio de nombres en todos los cheros que incluyan (include) dicho chero de encabezamiento y ello podra provocar colisiones inesperadas y no deseadas. Espacios de Nombres Annimos Los espacios de nombres annimos permiten denir entidades privadas internas a los mdulos de implementacin, de tal forma que no puedan producir colisiones con las entidades pblicas del sistema completo. De esta forma, cualquier declaracin y denicin realizada dentro de un espacio de nombres annimo ser nicamente visible en el mdulo de implementacin donde se encuentre (privada), pero no ser visible en el exterior del mdulo. Adicionalmente, tambin es posible denir espacios de nombres anidados (dentro de otros espacios de nombres), pudiendo, de esta forma, denir jerarquas de espacios de nombres.

Ejemplo. Mdulo Nmeros Complejos


A continuacin mostraremos todos los elementos estudiados en este captulo, presentando un programa en el que hacemos diversas operaciones con nmeros complejos. Es de suponer que, no solamente este programa, sino otros programas que podamos acometer en el futuro estarn interesados en trabajar con nmeros complejos. Por ese motivo, disearemos nuestra solucin mediante un mdulo principal, en el que se realizan las pruebas de las operaciones deseadas, y un mdulo complejos, que proporciona los tipos y operaciones necesarias para trabajar con nmeros complejos. De este modo, otros programas que deseen trabajar con este tipo de nmeros podrn reutilizar los tipos y operaciones que se describen en el interfaz del mdulo sin necesidad de volver a desarrollarlo. Bastar con incluir el interfaz del mdulo (complejos.hpp ) y enlazar su cdigo objeto (complejos.o ). El interfaz se dene en un chero de encabezamiento en el que nos aseguramos que no se produzca el problema de la doble inclusin. Utilizamos la directiva de compilacin condicional y elegimos como guarda el identicador _complejos_hpp_. Adems, denimos el espacio de nombres umalcc y nos aseguramos de que todas las entidades del interfaz estn dentro de dicho espacio de nombres. Como consecuencia, cualquier uso de una de dichas entidades exigir, bien una calicacin explta o bien el uso de la directiva using namespace umalcc. El interfaz del mdulo contiene la denicin del tipo Complejo y la declaracin de los prototipos de las operaciones que se pueden realizar con dicho tipo (leer, sumar, . . . ).
//- fichero: complejos.hpp -----------------------------------------#ifndef _complejos_hpp_ #define _complejos_hpp_ namespace umalcc { //---------------------------------struct Complejo { double real ; // parte real del numero complejo double imag ; // parte imaginaria del numero complejo } ; //---------------------------------void sumar(Complejo& r, const Complejo& a, const Complejo& b) ; // Devuelve un numero complejo (r) que contiene el resultado de // sumar los numeros complejos (a) y (b). //---------------------------------void restar(Complejo& r, const Complejo& a, const Complejo& b) ; // Devuelve un numero complejo (r) que contiene el resultado de // restar los numeros complejos (a) y (b). //---------------------------------void multiplicar(Complejo& r, const Complejo& a, const Complejo& b) ; // Devuelve un numero complejo (r) que contiene el resultado de // multiplicar los numeros complejos (a) y (b).
Dpto. Lenguajes y Ciencias de la Computacin Universidad de Mlaga

124

CAPTULO 10. MDULOS Y BIBLIOTECAS


//---------------------------------void dividir(Complejo& r, const Complejo& a, const Complejo& b) ; // Devuelve un numero complejo (r) que contiene el resultado de // dividir los numeros complejos (a) y (b). //---------------------------------bool iguales(const Complejo& a, const Complejo& b) ; // Devuelve true si los numeros complejos (a) y (b) son iguales. //---------------------------------void escribir(const Complejo& a) ; // muestra en pantalla el numero complejo (a) //---------------------------------void leer(Complejo& a) ; // lee de teclado el valor del numero complejo (a). // lee la parte real y la parte imaginaria del numero //---------------------------------} #endif //- fin: complejos.hpp ----------------------------------------------

La implementacin del mdulo deber relizarse en un chero independiente (complejos.cpp ), que debe incluir el correspondiente chero de encabezamiento para tener acceso a las deniciones y declaraciones all realizadas. Adems, como en este ejemplo necesitaremos efectuar entrada/salida con la consola, tambin incluimos la biblioteca estndar iostream. El uso de las entidades de dicha biblioteca exige utilizar el espacio de nombres std. En este caso, por comodidad optamos por usar la directiva using namespace lo que nos permitir omitir en el resto del chero el uso de calicacin explcita (std::).
//- fichero: complejos.cpp -----------------------------------------#include "complejos.hpp" #include <iostream> using namespace std ; using namespace umalcc ;

En este ejemplo la implementacin del mdulo consiste en la implementacin de los subprogramas cuyos prototipos fueron declarados en el chero de encabezamiento. La implementacin de dichos subprogramas puede utilizar otros subprogramas internos al mdulo como, por ejemplo, las funciones sq o abs, que se denen dentro de un espacio de nombres annimo, para hacerlas privadas al mdulo.
namespace { //---------------------------------//-- Subprogramas Auxiliares ------//---------------------------------// cuadrado de un numero (a^2) inline double sq(double a) { return a*a ; } //---------------------------------// Valor absoluto de un numero inline double abs(double a) { if (a < 0) { a = -a ; } return a ; } //---------------------------------// Dos numeros reales son iguales si la distancia que los
Dpto. Lenguajes y Ciencias de la Computacin Universidad de Mlaga

10.3. ESPACIOS DE NOMBRES


// separa es lo suficientemente pequenya inline bool iguales(double a, double b) { return abs(a-b) <= 1e-6 ; } }

125

Como las entidades del chero de encabezamiento fueron denidas en el espacio de nombres umalcc, ahora procedemos a su implementacin en dicho espacio de nombres. Los identicadores que dan nombre a dichas entidades son directamente accesibles en dicho espacio de nombres y, por tanto, pueden ser utilizados directamente, sin necesidad de calicacin (por ejemplo, Complejo).
namespace umalcc { //---------------------------------//-- Implementacin ---------------//---------------------------------// Devuelve un numero complejo (r) que contiene el resultado de // sumar los numeros complejos (a) y (b). void sumar(Complejo& r, const Complejo& a, const Complejo& b) { r.real = a.real + b.real ; r.imag = a.imag + b.imag ; } //---------------------------------// Devuelve un numero complejo (r) que contiene el resultado de // restar los numeros complejos (a) y (b). void restar(Complejo& r, const Complejo& a, const Complejo& b) { r.real = a.real - b.real ; r.imag = a.imag - b.imag ; } //---------------------------------// Devuelve un numero complejo (r) que contiene el resultado de // multiplicar los numeros complejos (a) y (b). void multiplicar(Complejo& r, const Complejo& a, const Complejo& b) { r.real = (a.real * b.real) - (a.imag * b.imag) ; r.imag = (a.real * b.imag) + (a.imag * b.real) ; } //---------------------------------// Devuelve un numero complejo (r) que contiene el resultado de // dividir los numeros complejos (a) y (b). void dividir(Complejo& r, const Complejo& a, const Complejo& b) { double divisor = sq(b.real) + sq(b.imag) ; if (iguales(0.0, divisor)) { r.real = 0 ; r.imag = 0 ; } else { r.real = ((a.real * b.real) + (a.imag * b.imag)) / divisor ; r.imag = ((a.imag * b.real) - (a.real * b.imag)) / divisor ; } } //---------------------------------// Devuelve true si los numeros complejos (a) y (b) son iguales. bool iguales(const Complejo& a, const Complejo& b) { return iguales(a.real, b.real) && iguales(a.imag, b.imag) ; } //---------------------------------Dpto. Lenguajes y Ciencias de la Computacin Universidad de Mlaga

126

CAPTULO 10. MDULOS Y BIBLIOTECAS


// muestra en pantalla el numero complejo (a) void escribir(const Complejo& a) { cout << "{ " << a.real << ", " << a.imag << " }" ; } //---------------------------------// lee de teclado el valor del numero complejo (a). // lee la parte real y la parte imaginaria del numero void leer(Complejo& a) { cin >> a.real >> a.imag ; } //---------------------------------} //- fin: complejos.cpp ----------------------------------------------

Una vez que disponemos del mdulo para operar con nmeros complejos, a continuacin mostramos un ejemplo de su utilizacin en el siguiente programa, que contiene algunos subprogramas para realizar pruebas de las operaciones bsicas del mismo. Nuestro mdulo principal comienza incluyendo la biblioteca estndar iostream, porque en el mismo utilizaremos operaciones de entrada/salida con la consola. Adems, declara variables de tipo Complejo y hace uso de diferentes subprogramas para trabajar con dicho tipo. Como consecuencia, debemos incluir el chero de encabezamiento con el interfaz de dicho mdulo (complejos.hpp ), consiguiendo as que todas sus deniciones y declaraciones sean visibles en el mdulo principal.
//- fichero: main.cpp -----------------------------------------#include <iostream> #include "complejos.hpp"

Sabemos que las entidades de la biblioteca iostream han sido denidas en el espacio de nombres std y las del mdulo complejos han sido denidas en el espacio de nombres umalcc. Utilizamos la directiva using namespace para evitar la necesidad de utilizar calicacin explcita cuando hagamos uso de las entidades denidas en las mismas. Sin embargo, hay una situacin en la que nos vemos obligados a utilizar calicacin explcita. Como se puede observar, el identicador leer tiene dos usos diferentes. Por una parte, es el nombre del subprograma utilizado para leer un nmero complejo, que ha sido declarado en el interfaz del mdulo (complejos.hpp ). Por otra parte, en el mdulo principal tenemos otro subprograma que tambin se llama leer, tiene los mismos parmetros, pero no tiene ninguna relacin con el primero. Si queremos utilizar el subprograma leer que ha sido denido en el interfaz del mdulo, es necesario que utilicemos el identicador el espacio de nombres (umalcc) para calicar explcitamente el nombre del subprograma, como se puede ver a continuacin. Ntese que, para invocar al subprograma leer del mdulo principal, se debe calicar explcitamente con :: directamente desde el espacio de nombres global.
using namespace std ; using namespace umalcc ; //-----------------------------------void leer(Complejo& c) { cout << "Introduzca un numero complejo { real img }: " ; umalcc::leer(c) ; }

A continuacin, se dene el resto de subprogramas adecuados para proceder a las pruebas deseadas y la funcin main.
//-----------------------------------void prueba_suma(const Complejo& c1, const Complejo& c2) {
Dpto. Lenguajes y Ciencias de la Computacin Universidad de Mlaga

10.3. ESPACIOS DE NOMBRES


Complejo c0 ; sumar(c0, c1, c2) ; escribir(c1) ; cout <<" + " ; escribir(c2) ; cout <<" = " ; escribir(c0) ; cout << endl ; Complejo aux ; restar(aux, c0, c2) ; if (! iguales(c1, aux)) { cout << "Error en operaciones de suma/resta"<< endl ; } } //-----------------------------------void prueba_resta(const Complejo& c1, const Complejo& c2) { Complejo c0 ; restar(c0, c1, c2) ; escribir(c1) ; cout <<" - " ; escribir(c2) ; cout <<" = " ; escribir(c0) ; cout << endl ; Complejo aux ; sumar(aux, c0, c2) ; if (! iguales(c1, aux)) { cout << "Error en operaciones de suma/resta"<< endl ; } } //-----------------------------------void prueba_mult(const Complejo& c1, const Complejo& c2) { Complejo c0 ; multiplicar(c0, c1, c2) ; escribir(c1) ; cout <<" * " ; escribir(c2) ; cout <<" = " ; escribir(c0) ; cout << endl ; Complejo aux ; dividir(aux, c0, c2) ; if (! iguales(c1, aux)) { cout << "Error en operaciones de mult/div"<< endl ; } } //-----------------------------------void prueba_div(const Complejo& c1, const Complejo& c2) { Complejo c0 ; dividir(c0, c1, c2) ; escribir(c1) ; cout <<" / " ; escribir(c2) ; cout <<" = " ; escribir(c0) ; cout << endl ;

127

Dpto. Lenguajes y Ciencias de la Computacin

Universidad de Mlaga

128

CAPTULO 10. MDULOS Y BIBLIOTECAS


Complejo aux ; multiplicar(aux, c0, c2) ; if (! iguales(c1, aux)) { cout << "Error en operaciones de mult/div"<< endl ; } } //-----------------------------------int main() { Complejo c1, c2 ; // calificacin explcita del espacio de nombres global // para evitar colisin en invocacin al subprograma // leer(Complejo& c) del espacio de nombres global ::leer(c1) ; ::leer(c2) ; //-------------------------------prueba_suma(c1, c2) ; prueba_resta(c1, c2) ; prueba_mult(c1, c2) ; prueba_div(c1, c2) ; //-------------------------------} //- fin: main.cpp ----------------------------------------------

Para nalizar, procederemos a su compilacin separada y a su enlazado utilizando GNU GCC :


g++ -ansi -Wall -Werror -c complejos.cpp g++ -ansi -Wall -Werror -c main.cpp g++ -ansi -Wall -Werror -o main main.o complejos.o

aunque alternativamente, dicho proceso tambin se puede realizar en dos pasos:


g++ -ansi -Wall -Werror -c complejos.cpp g++ -ansi -Wall -Werror -o main main.cpp complejos.o

o incluso en un nico paso:


g++ -ansi -Wall -Werror -o main main.cpp complejos.cpp

Dpto. Lenguajes y Ciencias de la Computacin

Universidad de Mlaga

Captulo 11

Tipos Abstractos de Datos


A medida que aumenta la complejidad del problema a resolver, del mismo modo deben aumentar los niveles de abstraccin necesarios para disear y construir su solucin algortmica. As, la abstraccin procedimental permite aplicar adecuadamente tcnicas de diseo descendente y renamientos sucesivos en el desarrollo de algoritmos y programas. La programacin modular permite aplicar la abstraccin a mayor escala, permitiendo abstraer sobre conjuntos de operaciones y los datos sobre los que se aplican. De esta forma, a medida que aumenta la complejidad del problema a resolver, aumenta tambin la complejidad de las estructuras de datos necesarias para su resolucin, y este hecho requiere, as mismo, la aplicacin de la abstraccin a las estructuras de datos. La aplicacin de la abstraccin a las estructuras de datos da lugar a los Tipos Abstractos de Datos (TAD), donde se especica el concepto que TAD representa un determinado tipo de datos, y la semntica (el signicado) de op1() las operaciones que se le pueden aplicar, pero donde su representacin e implementacin internas permanecen ocultas e inaccesibles desde el exterior, op2() de tal forma que no son necesarias para su utilizacin. As, podemos consiop3() derar que un tipo abstracto de datos encapsula una determinada estructura abstracta de datos, impidiendo su manipulacin directa, permitiendo solamente su manipulacin a travs de las operaciones especicadas. De este modo, los tipos abstractos de datos proporcionan un mecanismo adecuado para el diseo y reutilizacin de software able y robusto. Para un determinado tipo abstracto de datos, se pueden distinguir tres niveles: Nivel de utilizacin, donde se utilizan objetos de un determinado tipo abstracto de datos, basndose en la especicacin del mismo, de forma independiente a su implementacin y representacin concretas. As, estos objetos se manipulan mediante la invocacin a las operaciones especicadas en el TAD. Nivel de especicacin, donde se especica el tipo de datos, el concepto abstracto que representa y la semntica y restricciones de las operaciones que se le pueden aplicar. Este nivel representa el interfaz pblico del tipo abstracto de datos.

Utilizacin de TAD Especicacin de TAD Implementacin de TAD

Nivel de implementacin, donde se dene e implementa tanto las estructuras de datos que soportan la abstraccin, como las operaciones que actan sobre ella segn la semntica especicada. Este nivel interno permanece privado, y no es accesible desde el exterior del tipo abstracto de datos. Ntese que para una determinada especicacin de un tipo abstracto de datos, su implementacin puede cambiar sin que ello afecte a la utilizacin del mismo. 129

130

CAPTULO 11. TIPOS ABSTRACTOS DE DATOS

11.1.

Tipos Abstractos de Datos en C++: Clases

En el lenguaje de programacin C++, las clases dan la posibilidad al programador de denir tipos abstractos de datos, de tal forma que permiten denir su representacin interna (compuesta por sus atributos miembros), la forma en la que se crean y se destruyen, como se asignan y se pasan como parmetros, y las operaciones que se pueden aplicar (denominadas funciones miembro o simplemente mtodos). De esta forma se hace el lenguaje extensible. As mismo, la denicin de tipos abstractos de datos mediante clases puede ser combinada con la denicin de mdulos (vase 10), haciendo de este modo posible la reutilizacin de estos nuevos tipos de datos. As, en C++ una determinada clase dene un determinado tipo abstracto de datos, y un objeto se corresponde con una determinada instancia de una clase, de igual forma que una variable se corresponde con una determinada instancia de un tipo de datos. Aunque C++ permite implementar las clases utilizando una denicin en lnea, en este captulo nos centraremos en la implementacin separada de los mtodos de las clases. Adems, combinaremos la especicacin de la clase y su implementacin con los conceptos de programacin modular vistos en el captulo anterior (vase 10), de tal forma que la denicin de la clase se realizar en el chero de cabecera (hpp) de un determinado mdulo, y la implementacin de la clase se realizar en el chero de implementacin (cpp) del mdulo.

11.1.1.

Denicin de Clases

La denicin de la clase se realizar en el chero de cabecera (hpp) de un determinado mdulo, dentro de las guardas y espacio de nombres adecuado. Para ello, se especica la palabra reservada class seguida por el identicador de la nueva clase (tipo) que se est deniendo, y entre llaves la denicin de los atributos (miembros) que lo componen y de los mtodos (funciones miembros) que se le pueden aplicar directamente a los objetos de la clase. Finalmente el delimitador punto y coma (;) debe seguir al delimitador cierra-llaves (}).
//- fichero: complejos.hpp -----------------------------------------#ifndef _complejos_hpp_ #define _complejos_hpp_ namespace umalcc { class Complejo { // ... } ; } #endif //- fin: complejos.hpp ----------------------------------------------

Zona Privada y Zona Pblica En la denicin de una clase, se pueden distinguir dos mbitos de visibilidad (accesibilidad), la parte privada, cuyos miembros slo sern accesibles desde un mbito interno a la propia clase, y la parte pblica, cuyos miembros son accesibles tanto desde un mbito interno como desde un mbito externo a la clase. La parte privada comprende desde el principio de la denicin de la clase hasta la etiqueta public:, y la parte pblica comprende desde esta etiqueta hasta que se encuentra otra etiqueta private:. Cada vez que se especica una de las palabras reservadas public: o private:, las declaraciones que la siguen adquieren el atributo de visibilidad dependiendo de la etiqueta especicada.
class Complejo { public: // ... zona pblica ... private: // ... zona privada ... } ;
Dpto. Lenguajes y Ciencias de la Computacin Universidad de Mlaga

11.1. TIPOS ABSTRACTOS DE DATOS EN C++: CLASES Atributos

131

Los atributos componen la representacin interna de la clase, y se denen usualmente en la zona de visibilidad privada de la clase, con objeto de proteger el acceso a dicha representacin interna. Su denicin se realiza de igual forma a los campos de los registros (vase 6.3). De igual modo a los registros y sus campos, cada objeto que se dena del tipo de la clase, almacenar su propia representacin interna de los atributos de forma independiente a los otros objetos (instancias de la misma clase).
class Complejo { public: // ... private: double real ; // parte real del numero complejo double imag ; // parte imaginaria del numero complejo } ;

El Constructor por Defecto El constructor de una clase permite construir e inicializar un objeto. El constructor por defecto es el mecanismo por defecto utilizado para construir objetos de este tipo cuando no se especica ninguna forma explcita de construccin. As, ser el encargado de construir el objeto con los valores iniciales adecuados en el momento en que sea necesaria dicha construccin, por ejemplo cuando el ujo de ejecucin alcanza la declaracin de una variable de dicho tipo (vase 11.1.2). Los constructores se declaran con el mismo identicador de la clase, seguidamente se especican entre parntesis los parmetros necesarios para la construccin, que en el caso del constructor por defecto, sern vacos.
class Complejo { public: Complejo() ; // ... private: // ... } ;

// Constructor por Defecto

Mtodos Generales y Mtodos Constantes Los mtodos se corresponden con las operaciones que permiten manipular de muy diversa forma el estado interno de un determinado objeto como instancia de una determinada clase. Puede haber mtodos denidos en el mbito pblico de la clase, en cuyo caso podrn ser invocados tanto desde mtodos internos de la clase, como desde el exterior de la clase, y mtodos denidos en el mbito privado de la clase, en cuyo caso slo podrn ser invocados desde mtodos internos de la clase. Estos mtodos denidos en el mbito privado de la clase suelen ser denidos como mtodos auxiliares que facilitan la implementacin de otros mtodos ms complejos. Los mtodos se declaran como los prototipos de los subprogramas (vase 5.7), pero teniendo en cuenta que son aplicados a un objeto instancia de la clase a la que pertenece, y que, por lo tanto, no es necesario que dicho objeto sea recibido como parmetro. Los mtodos de una clase pueden tener el calicador const especicado despus de los parmetros, en cuyo caso indica que el mtodo no modica el estado interno del objeto (atributos), por lo que se puede aplicar tanto a objetos constantes como variables. En otro caso, si dicho calicador const no aparece, entonces signica que el mtodo si modica el estado interno del objeto (atributos), por lo que slo podr ser aplicado a objetos variables, y por el contrario no podr ser aplicado a objetos constantes.
class Complejo { public: // ...
Dpto. Lenguajes y Ciencias de la Computacin Universidad de Mlaga

132

CAPTULO 11. TIPOS ABSTRACTOS DE DATOS


//---------------------------void sumar(const Complejo& a, const Complejo& b) ; // asigna al numero complejo (actual) el resultado de // sumar los numeros complejos (a) y (b). //---------------------------bool igual(const Complejo& b) const ; // Devuelve true si el numero complejo (actual) es // igual al numero complejo (b) //---------------------------void escribir() const ; // muestra en pantalla el numero complejo (actual) //---------------------------void leer() ; // lee de teclado el valor del numero complejo (actual). // lee la parte real y la parte imaginaria del numero //-----------------------------// ... private: // ... } ;

11.1.2.

Utilizacin de Clases

Un tipo abstracto de datos, denido como una clase encapsulada dentro de un mdulo, puede ser utilizado por cualquier otro mdulo que lo necesite. Para ello, deber incluir el chero de cabecera donde se encuentra la denicin de la clase, y podr denir tantos objetos (instancias de dicha clase) como sean necesarios, para ello, deber utilizar calicacin explcita o implcita dependiendo del contexto de su utilizacin (cheros de encabezamiento o de implementacin respectivamente). Instancias de Clase: Objetos Un objeto es una instancia de una clase, y podremos denir tantos objetos cuyo tipo sea de una determinada clase como sea necesario, de tal modo que cada objeto contiene su propia representacin interna (atributos) de forma independiente del resto. La denicin de un objeto de una determinada clase se realiza de igual forma a la denicin de una variable (o constante) de un determinado tipo, de tal forma que cada objeto ser una instancia independiente de una determinada clase (tipo abstracto de datos).
//- fichero: main.cpp ----------------------------------------------#include <iostream> #include "complejos.hpp" using namespace std ; using namespace umalcc ; int main() { 0.0 0.0 real: real: Complejo c1, c2 ; imag: imag: 0.0 0.0 // ... c1 c2 } //- fin: main.cpp ---------------------------------------------------

Es importante remarcar que cada objeto, denido de una determinada clase, es una instancia independiente de los otros objetos denidos de la misma clase, con su propia memoria para contener de forma independiente el estado de su representacin interna. Tiempo de Vida de los Objetos Durante la ejecucin del programa, cuando el ujo de ejecucin llega a la sentencia donde se dene un determinado objeto, entonces se reserva espacio en memoria para contener a dicho objeto,
Dpto. Lenguajes y Ciencias de la Computacin Universidad de Mlaga

11.1. TIPOS ABSTRACTOS DE DATOS EN C++: CLASES

133

y se invoca al constructor especicado (si no se especica ningn constructor, entonces se invoca al constructor por defecto) para construir adecuadamente al objeto, siendo de esta forma accesible desde este punto de construccin hasta que el ujo de ejecucin alcanza el nal de bloque donde el objeto ha sido denido, en cuyo caso el objeto se destruye (invocando a su destructor ) y se libera la memoria que ocupaba, pasando de este modo a estar inaccesible.
//- fichero: main.cpp ----------------------------------------------#include <iostream> #include "complejos.hpp" using namespace std ; using namespace umalcc ; int main() { Complejo c1 ; // construccin de c1 (1 vez) for (int i = 0 ; i < 3 ; ++i) { Complejo c2 ; // construccin de c2 (3 veces) // ... } // destruccin de c2 (3 veces) // ... } // destruccin de c1 (1 vez) //- fin: main.cpp ---------------------------------------------------

Manipulacin de los Objetos Una vez que un objeto es accesible, se puede manipular invocando a los mtodos pblicos denidos en su interfaz. Esta invocacin de los mtodos se aplica sobre un determinado objeto en concreto, y se realiza especicando el identicador del objeto sobre el que recae la invocacin al mtodo, seguido por el smbolo punto (.) y por la invocacin al mtodo en cuestin, es decir, el identicador del mtodo y los parmetros actuales necesarios entre parntesis. Hay que tener en cuenta que a los objetos constantes slo se les podrn aplicar mtodos constantes. As mismo, ntese que no es posible acceder a los atributos ni mtodos que hayan sido denidos en la parte privada de la clase.
//- fichero: main.cpp ----------------------------------------------#include <iostream> #include "complejos.hpp" using namespace std ; using namespace umalcc ; int main() { Complejo c1, c2, c3 ; // construccin de c1, c2, c3 c1.leer() ; 5.3 2.5 real: real: real: c2.leer() ; imag: imag: imag: 2.4 7.3 c3.sumar(c1, c2) ; c1 c2 c3.escribir() ; } // destruccin de c1, c2, c3 //- fin: main.cpp ---------------------------------------------------

7.8 9.7 c3

Paso de Parmetros de Objetos Es importante considerar que las clases (tipos abstractos de datos) son tipos compuestos, y por lo tanto deben seguir las mismas convenciones para el paso de parmetros de tipos compuestos (vase 6.1), es decir, los parmetros de salida o entrada/salida se pasan por referencia, y los parmetros de entrada se pasan por referencia constante.
Dpto. Lenguajes y Ciencias de la Computacin Universidad de Mlaga

134

CAPTULO 11. TIPOS ABSTRACTOS DE DATOS

11.1.3.

Implementacin de Clases

La implementacin de los mtodos de la clase se realizar en el chero de implementacin (cpp) del mdulo correspondiente, dentro del mismo espacio de nombres en el que fue realizada la denicin de la clase en el chero de cabecera. En el chero de implementacin se podrn denir, dentro del espacio de nombres adecuado, las constantes, tipos y subprogramas auxiliares necesarios que nos faciliten la implementacin de los mtodos de la clase. Para implementar un determinado constructor o mtodo de la clase, dentro del mismo espacio de nombres que la denicin de la clase, se calicar explcitamente el identidor del mtodo con el identicador de la clase a la que pertenece.
//- fichero: complejos.cpp -----------------------------------------#include "complejos.hpp" namespace umalcc { // ... //---------------------------Complejo::Complejo() // Constructor por Defecto // ... //---------------------------void Complejo::sumar(const Complejo& a, const Complejo& b) // ... //---------------------------bool Complejo::igual(const Complejo& b) const // ... //---------------------------void Complejo::escribir() const // ... //---------------------------void Complejo::leer() // ... //---------------------------// ... } //- fin: complejos.cpp ----------------------------------------------

Mtodos En la implementacin de un determinado mtodo de una clase, ste mtodo puede invocar directamente a cualquier otro mtodo de la clase sin necesidad de aplicar el operador punto (.). As mismo, un mtodo de la clase puede acceder directamente a los atributos del objeto sobre el que se invoque dicho mtodo, sin necesidad de aplicar el operador punto (.), ni necesidad de recibirlo como parmetro. Por ejemplo:
void Complejo::sumar(const Complejo& a, const Complejo& b) { real = a.real + b.real ; imag = a.imag + b.imag ; }

Sin embargo, para acceder a los atributos de los objetos recibidos como parmetros, si son accesibles desde la implementacin de una determinada clase, es necesario especicar el objeto (mediante su identicador) seguido por el operador punto (.) y a continuacin el identicador del atributo en cuestin. As, podemos ver como para calcular la suma de nmeros complejos, se asigna a las partes real e imaginaria del nmero complejo que estamos calculando (el objeto sobre el que se aplica el mtodo sumar) la suma de las partes real e imaginaria respectivamente de los nmeros complejos que recibe como parmetros. Por ejemplo, cuando se ejecuta la sentencia:
Dpto. Lenguajes y Ciencias de la Computacin Universidad de Mlaga

11.1. TIPOS ABSTRACTOS DE DATOS EN C++: CLASES


c3.sumar(c1, c2) ;

135

la sentencia correspondiente a la implementacin del mtodo sumar(...):


real = a.real + b.real ;

almacenar en el atributo real del nmero complejo c3 el resultado de sumar los valores del atributo real de los nmeros complejos c1 y c2. De igual modo suceder con el atributo imag del nmero complejo c3, que almacenar el resultado de sumar los valores del atributo imag de los nmeros complejos c1 y c2. Constructores En la implementacin de los constructores de la clase, tambin ser calicado explcitamente con el identicador de la clase correspondiente. Despus de la denicin de los parmetros, a continuacin del delimitador (:), se especica la lista de inicializacin, donde aparecen, separados por comas y segn el orden de declaracin, todos los atributos miembros del objeto, as como los valores con los que sern inicializados especicados entre parntesis (se invoca al constructor adecuado segn los parmetros especicados entre parntesis, de tal forma que los parntesis vacos representan la construccin por defecto). A continuacin se especican entre llaves las sentencias pertenecientes al cuerpo del constructor para realizar las acciones adicionales necesarias para la construccin del objeto. Si no es necesario realizar ninguna accin adicional, entonces el cuerpo del constructor se dejar vaco. Por ejemplo, implementaremos el constructor por defecto de la clase Complejo para que inicialice cada atributo (parte real e imaginaria) del objeto que se construya invocando a su propio constructor por defecto respectivamente (segn su tipo):
Complejo::Complejo() : real(), imag() { } // Constructor por Defecto

Alternativamente, la siguiente implementacin es equivalente, para que inicialice cada atributo (parte real e imaginaria) del objeto que se construya con el valor cero (0.0) especicado entre parntesis.
Complejo::Complejo() : real(0.0), imag(0.0) { } // Constructor por Defecto

11.1.4.

Ejemplo

Por ejemplo, el TAD nmero complejo representa el siguiente concepto matemtico de nmero complejo: Un nmero complejo representa un punto en el plano complejo, compuesto por dos componentes que representan la parte real y la parte imaginaria del nmero (abcisa y ordenada respectivamente en el plano cartesiano), al cual se le pueden aplicar las operaciones de suma, resta, multiplicacin y divisin, as como la comparacin de igualdad. Denicin
//- fichero: complejos.hpp -----------------------------------------#ifndef _complejos_hpp_ #define _complejos_hpp_ namespace umalcc { //---------------------------------const double ERROR_PRECISION = 1e-6 ; //---------------------------------class Complejo { public: //---------------------------------------------------------Dpto. Lenguajes y Ciencias de la Computacin Universidad de Mlaga

136

CAPTULO 11. TIPOS ABSTRACTOS DE DATOS


//-- Mtodos Pblicos -------------------------------------//---------------------------------------------------------Complejo() ; // Constructor por Defecto //---------------------------double parte_real() const ; // devuelve la parte real del numero complejo //---------------------------double parte_imag() const ; // devuelve la parte imaginaria del numero complejo //---------------------------void sumar(const Complejo& a, const Complejo& b) ; // asigna al numero complejo (actual) el resultado de // sumar los numeros complejos (a) y (b). //---------------------------void restar(const Complejo& a, const Complejo& b) ; // asigna al numero complejo (actual) el resultado de // restar los numeros complejos (a) y (b). //---------------------------void multiplicar(const Complejo& a, const Complejo& b) ; // asigna al numero complejo (actual) el resultado de // multiplicar los numeros complejos (a) y (b). //---------------------------void dividir(const Complejo& a, const Complejo& b) ; // asigna al numero complejo (actual) el resultado de // dividir los numeros complejos (a) y (b). //---------------------------bool igual(const Complejo& b) const ; // Devuelve true si el numero complejo (actual) es // igual al numero complejo (b) //---------------------------void escribir() const ; // muestra en pantalla el numero complejo (actual) //---------------------------void leer() ; // lee de teclado el valor del numero complejo (actual). // lee la parte real y la parte imaginaria del numero //-----------------------------private: //---------------------------------------------------------//-- Atributos Privados -----------------------------------//---------------------------------------------------------double real ; // parte real del numero complejo double imag ; // parte imaginaria del numero complejo //---------------------------------------------------------} ; } #endif //- fin: complejos.hpp ----------------------------------------------

Implementacin
//- fichero: complejos.cpp -----------------------------------------#include "complejos.hpp" #include <iostream> using namespace std ; using namespace umalcc ; //-----------------------------------------------------------------------------// Espacio de nombres anonimo. Es una parte privada de la // implementacion. No es accesible desde fuera del modulo
Dpto. Lenguajes y Ciencias de la Computacin Universidad de Mlaga

11.1. TIPOS ABSTRACTOS DE DATOS EN C++: CLASES


//-----------------------------------------------------------------------------namespace { //---------------------------------------------------------//-- Subprogramas Auxiliares ------------------------------//---------------------------------------------------------// cuadrado de un numero (a^2) inline double sq(double a) { return a*a ; } //------------------------// Valor absoluto de un numero inline double abs(double a) { return (a >= 0) ? a : -a ; } //------------------------// Dos numeros reales son iguales si la distancia que los // separa es lo suficientemente pequenya inline bool iguales(double a, double b) { return abs(a-b) <= ERROR_PRECISION ; } } //-----------------------------------------------------------------------------// Espacio de nombres umalcc. // Aqui reside la implementacion de la parte publica del modulo //-----------------------------------------------------------------------------namespace umalcc { //---------------------------------------------------------//-- Mtodos Pblicos -------------------------------------//---------------------------------------------------------Complejo::Complejo() // Constructor por Defecto : real(0.0), imag(0.0) { } //---------------------------// devuelve la parte real del numero complejo double Complejo::parte_real() const { return real ; } //---------------------------// devuelve la parte imaginaria del numero complejo double Complejo::parte_imag() const { return imag ; } //---------------------------// asigna al numero complejo (actual) el resultado de // sumar los numeros complejos (a) y (b). void Complejo::sumar(const Complejo& a, const Complejo& b) { real = a.real + b.real ; imag = a.imag + b.imag ; } //---------------------------// asigna al numero complejo (actual) el resultado de // restar los numeros complejos (a) y (b). void Complejo::restar(const Complejo& a, const Complejo& b) {

137

Dpto. Lenguajes y Ciencias de la Computacin

Universidad de Mlaga

138
real = a.real - b.real ; imag = a.imag - b.imag ;

CAPTULO 11. TIPOS ABSTRACTOS DE DATOS

} //---------------------------// asigna al numero complejo (actual) el resultado de // multiplicar los numeros complejos (a) y (b). void Complejo::multiplicar(const Complejo& a, const Complejo& b) { real = (a.real * b.real) - (a.imag * b.imag) ; imag = (a.real * b.imag) + (a.imag * b.real) ; } //---------------------------// asigna al numero complejo (actual) el resultado de // dividir los numeros complejos (a) y (b). void Complejo::dividir(const Complejo& a, const Complejo& b) { double divisor = sq(b.real) + sq(b.imag) ; if (iguales(0.0, divisor)) { real = 0.0 ; imag = 0.0 ; } else { real = ((a.real * b.real) + (a.imag * b.imag)) / divisor ; imag = ((a.imag * b.real) - (a.real * b.imag)) / divisor ; } } //---------------------------// Devuelve true si el numero complejo (actual) es // igual al numero complejo (b) bool Complejo::igual(const Complejo& b) const { return iguales(real, b.real) && iguales(imag, b.imag) ; } //---------------------------// muestra en pantalla el numero complejo (actual) void Complejo::escribir() const { cout << "{ " << real << ", " << imag << " }" ; } //---------------------------// lee de teclado el valor del numero complejo (actual). // lee la parte real y la parte imaginaria del numero void Complejo::leer() { cin >> real >> imag ; } //---------------------------} //- fin: complejos.cpp ----------------------------------------------

Utilizacin
//- fichero: main.cpp ----------------------------------------------#include <iostream> #include "complejos.hpp" using namespace std ; using namespace umalcc ; //-----------------------------------void leer(Complejo& c) {
Dpto. Lenguajes y Ciencias de la Computacin Universidad de Mlaga

11.1. TIPOS ABSTRACTOS DE DATOS EN C++: CLASES


cout << "Introduzca un numero complejo { real img }: " ; c.leer() ; } //-----------------------------------void prueba_suma(const Complejo& c1, const Complejo& c2) { Complejo c0 ; c0.sumar(c1, c2) ; c1.escribir() ; cout <<" + " ; c2.escribir() ; cout <<" = " ; c0.escribir() ; cout << endl ; Complejo aux ; aux.restar(c0, c2) ; if ( ! c1.igual(aux)) { cout << "Error en operaciones de suma/resta"<< endl ; } } //-----------------------------------void prueba_resta(const Complejo& c1, const Complejo& c2) { Complejo c0 ; c0.restar(c1, c2) ; c1.escribir() ; cout <<" - " ; c2.escribir() ; cout <<" = " ; c0.escribir() ; cout << endl ; Complejo aux ; aux.sumar(c0, c2) ; if ( ! c1.igual(aux)) { cout << "Error en operaciones de suma/resta"<< endl ; } } //-----------------------------------void prueba_mult(const Complejo& c1, const Complejo& c2) { Complejo c0 ; c0.multiplicar(c1, c2) ; c1.escribir() ; cout <<" * " ; c2.escribir() ; cout <<" = " ; c0.escribir() ; cout << endl ; Complejo aux ; aux.dividir(c0, c2) ; if ( ! c1.igual(aux)) { cout << "Error en operaciones de mult/div"<< endl ; } } //-----------------------------------void prueba_div(const Complejo& c1, const Complejo& c2) { Complejo c0 ; c0.dividir(c1, c2) ;

139

Dpto. Lenguajes y Ciencias de la Computacin

Universidad de Mlaga

140

CAPTULO 11. TIPOS ABSTRACTOS DE DATOS


c1.escribir() ; cout <<" / " ; c2.escribir() ; cout <<" = " ; c0.escribir() ; cout << endl ; Complejo aux ; aux.multiplicar(c0, c2) ; if ( ! c1.igual(aux)) { cout << "Error en operaciones de mult/div"<< endl ; } } //-----------------------------------int main() { Complejo c1, c2 ; leer(c1) ; leer(c2) ; //-------------------------------prueba_suma(c1, c2) ; prueba_resta(c1, c2) ; prueba_mult(c1, c2) ; prueba_div(c1, c2) ; //-------------------------------} //- fin: main.cpp ---------------------------------------------------

11.2.

Tipos Abstractos de Datos en C++: Ms sobre Clases

Constantes de mbito de Clase Las constantes de mbito de clase se denen especicando los calicadores static const, seguidos por el tipo, el identicador y el valor de la constante. Estas constantes sern comunes y accesibles a todas las instancias (objetos) de la clase. Por ejemplo, para denir la constante MAX con un valor de 256 en la zona privada de la clase:
class ListaInt { public: // ... private: static const int MAX = 256 ; // ... } ;

Usualmente las constantes se denen en la zona privada de la clase, por lo que usualmente slo sern accesibles internamente desde dentro de la clase. Sin embargo, en algunas situaciones puede ser conveniente denir la constante en la zona pblica de la clase, entonces en este caso la constante podr ser accedida desde el exterior de la clase, y ser utilizada mediante calicacin explcita utilizando el identicador de la clase. Por ejemplo:
class ListaInt { public: static const int MAX = 256 ; // ... private: // ... } ; // ... int main()
Dpto. Lenguajes y Ciencias de la Computacin Universidad de Mlaga

11.2. TIPOS ABSTRACTOS DE DATOS EN C++: MS SOBRE CLASES


{ int x = ListaInt::MAX ; // ... }

141

Sin embargo, la denicin de constantes de mbito de clase de tipos diferentes a los integrales (char, short, int, unsigned, long), por ejemplo float y double es un poco ms compleja, por lo que usualmente se realizar externamente a la denicin de la clase, dentro del mbito del mdulo (en el chero hpp, dentro del espacio de nombres del mdulo, si debe ser pblica, y en el chero cpp, dentro del espacio de nombres annimo, si debe ser privada. Tipos de mbito de Clase Tambin se pueden denir tipos internos de mbito de clase de igual forma a como se hace externamente a la clase, pero en este caso su mbito de visibilidad estar restringido a la clase donde se dena. Estos tipos sern tiles en la denicin de los atributos miembros de la clase, o para denir elementos auxiliares en la implementacin del tipo abstracto de datos. Por ejemplo, para denir un tipo Datos como un array de 256 nmeros enteros:
#include <tr1/array> // ... class ListaInt { public: // ... private: static const int MAX = 256 ; typedef std::tr1::array<int, MAX> Datos ; struct Elemento { // ... } ; // ... } ;

Usualmente los tipos se denen en la zona privada de la clase, por lo que usualmente slo sern accesibles internamente desde dentro de la clase. Sin embargo, en algunas situaciones puede ser conveniente denir el tipo en la zona pblica de la clase, entonces en este caso el tipo podr ser accedido desde el exterior de la clase, y ser utilizado mediante calicacin explcita utilizando el identicador de la clase. Por ejemplo:
#include <tr1/array> // ... class ListaInt { public: static const int MAX = 256 ; typedef std::tr1::array<int, MAX> Datos ; // ... private: // ... } ; // ... int main() { ListaInt::Datos d ; // ... }

Ntese que los tipos deben ser pblicos si forman parte de los parmetros de los mtodos pblicos, de tal forma que puedan ser utilizados externamente, all donde sea necesario invocar a dichos mtodos pblicos.
Dpto. Lenguajes y Ciencias de la Computacin Universidad de Mlaga

142 Constructores Especcos

CAPTULO 11. TIPOS ABSTRACTOS DE DATOS

Los constructores de una clase permiten construir e inicializar un objeto. Anteriormente se ha explicado el constructor por defecto, el cual se invoca cuando se crea un determinado objeto y no se especica que tipo de construccin se debe realizar. C++ permite, adems, la denicin e implementacin de tantos constructores especcos como sean necesarios, para ello, se debe especicar en la lista de parmetros, aquellos que sean necesarios para poder construir el objeto adecuadamente en cada circunstancia especca, de tal forma que ser la lista de parmetros formales la que permita discriminar que constructor ser invocado dependiendo de los parmetros actuales utilizados en la invocacin al constructor. Por ejemplo, podemos denir un constructor especco para que reciba dos nmeros reales como parmetros (parte real e imaginaria respectivamente de un nmero complejo), los cuales sern utilizados para dar los valores iniciales a cada atributo correspondiente del objeto que se construya. As, su denicin podra ser:
class Complejo { public: Complejo(double p_real, double p_imag) ; } ;

// Constructor Especfico

A continuacin se puede ver como sera la implementacin de este constructor especco, donde se inicializa el valor de cada atributo con el valor de cada parmetro recibido en la invocacin de la construccin del objeto.
Complejo::Complejo(double p_real, double p_imag) : real(p_real), imag(p_imag) { } // Constructor especfico

Finalmente, a continuacin podemos ver un ejemplo de como sera una posible invocacin a dicho constructor especco (para c2), junto a una invocacin al constructor por defecto (para c1):
//- fichero: main.cpp ----------------------------------------------#include <iostream> #include "complejos.hpp" using namespace std ; using namespace umalcc ; int main() { 0.0 2.5 real: real: Complejo c1 ; imag: imag: 0.0 7.3 Complejo c2(2.5, 7.3) ; c1 c2 // ... } //- fin: main.cpp ---------------------------------------------------

Constructor por Defecto Como se explic anteriormente (vase 11.1.1 y 11.1.3), el constructor por defecto es el mecanismo por defecto utilizado para construir objetos de este tipo cuando no se especica ninguna forma explcita de construccin. As, ser invocado automticamente cuando se deba construir un determinado objeto, sin especicar explcitamente el tipo de construccin requerido, en el momento en que sea necesaria dicha construccin, por ejemplo cuando el ujo de ejecucin alcanza la declaracin de una variable de dicho tipo (vase 11.1.2). El constructor por defecto es un mtodo especial de la clase, ya que si el programador no dene ningn constructor para una determinada clase, entonces el compilador generar e implementar automticamente dicho constructor con el comportamiento por defecto de invocar automticamente al constructor por defecto para cada atributo de tipo compuesto miembro de la clase. Ntese, sin embargo, que en el caso atributos de tipo simple, la implementacin automtica del compilador los dejar sin inicializar. No obstante, el programador puede denir el constructor por defecto para una determinada clase cuando el comportamiento generado automticamente por el compilador no sea el deseado.
Dpto. Lenguajes y Ciencias de la Computacin Universidad de Mlaga

11.2. TIPOS ABSTRACTOS DE DATOS EN C++: MS SOBRE CLASES

143

Para ello, la denicin del constructor por defecto se corresponde con la denicin de un constructor que no recibe ningn parmetro, y la implementacin depender de las acciones necesarias para inicializar por defecto el estado interno del objeto que se est creando. Por ejemplo, para la clase Complejo:
class Complejo { public: Complejo() ; } ;

// Constructor por Defecto

A continuacin se puede ver como sera la implementacin del constructor por defecto:
Complejo::Complejo() // Constructor por Defecto : real(0.0), imag(0.0) { }

Otra posible implementacin podra ser la siguiente, que invoca explcitamente al constructor por defecto para cada atributo miembro de la clase (que en este caso se incializar a cero):
Complejo::Complejo() : real(), imag() { } // Constructor por Defecto

Finalmente, a continuacin podemos ver un ejemplo de como sera una invocacin a dicho constructor por defecto:
//- fichero: main.cpp ----------------------------------------------#include <iostream> #include "complejos.hpp" using namespace std ; using namespace umalcc ; int main() { 0.0 0.0 real: real: Complejo c1, c2 ; imag: imag: 0.0 0.0 // ... c1 c2 } //- fin: main.cpp ---------------------------------------------------

Constructor de Copia El constructor de copia es el constructor que permite inicializar un determinado objeto como una copia de otro objeto de su misma clase. As, se invoca automaticamente al inicializar el contenido de un objeto con el valor de otro objeto de su misma clase, y tambin es invocado automticamente cuando un objeto de dicho tipo se pasa como parmetro por valor a subprogramas, aunque esto ltimo, como se ha explicado previamente, est desaconsejado, ya que lo usual es pasar los tipos compuestos por referencia o por referencia constante. El constructor de copia es un mtodo especial de la clase, ya que si el programador no dene dicho constructor de copia para una determinada clase, entonces el compilador generar e implementar automticamente dicho constructor de copia con el comportamiento por defecto de invocar automticamente al constructor de copia para cada atributo miembro de la clase, en este caso, tanto para atributos de tipo simple como de tipo compuesto. No obstante, el programador puede denir el constructor de copia para una determinada clase cuando el comportamiento generado automticamente por el compilador no sea el deseado. Para ello, la denicin del constructor de copia se corresponde con la denicin de un constructor que recibe como nico parmetro por referencia constante un objeto del mismo tipo que la clase del constructor, y la implementacin depender de las acciones necesarias para copiar el estado interno del objeto recibido como parmetro al objeto que se est creando. Por ejemplo, para la clase Complejo:
class Complejo { public: Complejo(const Complejo& c) ; } ;
Dpto. Lenguajes y Ciencias de la Computacin

// Constructor de Copia

Universidad de Mlaga

144

CAPTULO 11. TIPOS ABSTRACTOS DE DATOS

y su implementacin podra ser la siguiente, que en este caso coincide con la implementacin que generara automaticamente el compilador en caso de que no fuese implementado por el programador:
Complejo::Complejo(const Complejo& o) : real(o.real), imag(o.imag) { } // Constructor de Copia // Implementacin automtica

Finalmente, a continuacin podemos ver un ejemplo de como sera una invocacin al constructor de copia (para c3 y c4), junto a una invocacin a un constructor especco (para c2) y una invocacin al constructor por defecto (para c1), as como la construccin por copia (para c5 y c6) de objetos construidos invocando explcitamente a los constructores adecuados:
//- fichero: main.cpp ----------------------------------------------#include <iostream> #include "complejos.hpp" using namespace std ; using namespace umalcc ; real: 0.0 2.5 0.0 2.5 0.0 3.1 imag: 0.0 7.3 0.0 7.3 0.0 4.2 int main() c1 c2 c3 c4 c5 c6 { Complejo c1 ; // Construccin por defecto Complejo c2(2.5, 7.3) ; // Construccin especfica Complejo c3(c1) ; // Construccin de copia (de c1) Complejo c4 = c2 ; // Construccin de copia (de c2) Complejo c5 = Complejo(); // Construccin de copia de Complejo por Defecto Complejo c6 = Complejo(3.1, 4.2); // Construccin de copia de Complejo Especfico // ... } //- fin: main.cpp ---------------------------------------------------

Destructor El destructor de una clase ser invocado automticamente (sin parmetros actuales) para una determinada instancia (objeto) de esta clase cuando dicho objeto deba ser destruido, normalmente sto suceder cuando el ujo de ejecucin del programa salga del mbito de visibilidad de dicho objeto (vase 11.1.2). El destructor es un mtodo especial de la clase, ya que si el programador no dene dicho destructor para una determinada clase, entonces el compilador generar e implementar automticamente dicho destructor con el comportamiento por defecto de invocar automticamente al destructor para cada atributo de tipo compuesto miembro de la clase. No obstante, el programador puede denir el destructor para una determinada clase cuando el comportamiento generado automticamente por el compilador no sea el deseado. Para ello, el destructor de la clase se dene mediante el smbolo ~ seguido del identicador de la clase y una lista de parmetros vaca, y la implementacin depender de las acciones necesarias para destruir y liberar los recursos asociados al estado interno del objeto que se est destruyendo. Posteriormente, el destructor invoca automticamente a los destructores de los atributos miembros del objeto para que stos sean destruidos. Por ejemplo, para la clase Complejo:
class Complejo { public: ~Complejo() ; } ;

// Destructor

y su implementacin podra ser la siguiente, que en este caso coincide con la implementacin que generara automaticamente el compilador en caso de que no fuese implementado por el programador:
Complejo::~Complejo() { } // Destructor: Implementacin automtica

Finalmente, a continuacin podemos ver un ejemplo de como se invoca automticamente al destructor de los objetos cuando termina su tiempo de vida (para c1, c2, c3 y c4):
Dpto. Lenguajes y Ciencias de la Computacin Universidad de Mlaga

11.2. TIPOS ABSTRACTOS DE DATOS EN C++: MS SOBRE CLASES


//- fichero: main.cpp ----------------------------------------------#include <iostream> #include "complejos.hpp" using namespace std ; using namespace umalcc ; real: 0.0 2.5 0.0 real: real: real: imag: imag: imag: imag: 0.0 7.3 0.0 int main() c1 c2 c3 { Complejo c1 ; // Construccin por defecto Complejo c2(2.5, 7.3) ; // Construccin especfica Complejo c3(c1) ; // Construccin de copia (de c1) Complejo c4 = c2 ; // Construccin de copia (de c2) // ... } // Destruccin automtica de c4, c3, c2 y c1 //- fin: main.cpp ---------------------------------------------------

145

X
2.5 7.3 c4

Operador de Asignacin El operador de asignacin dene como se realiza la asignacin (=) para objetos de esta clase. No se debe confundir el operador de asignacin con el constructor de copia, ya que el constructor de copia construye un nuevo objeto que no tiene previamente ningn valor, mientras que en el caso del operador de asignacin, el objeto ya tiene previamente un valor que deber ser sustituido por el nuevo valor. Este valor previo deber, en ocasiones, ser destruido antes de realizar la asignacin del nuevo valor. El operador de asignacin (=) es un mtodo especial de la clase, ya que si el programador no dene dicho operador de asignacin para una determinada clase, entonces el compilador generar e implementar automticamente dicho operador de asignacin con el comportamiento por defecto de invocar automticamente al operador de asignacin para cada atributo miembro de la clase, tanto para atributos de tipo simple como de tipo compuesto. No obstante, el programador puede denir el operador de asignacin para una determinada clase cuando el comportamiento generado automticamente por el compilador no sea el deseado. Para ello, la denicin del operador de asignacin se corresponde con la denicin de un operador = que recibe como nico parmetro por referencia constante un objeto del mismo tipo que la clase del constructor, devuelve una referencia al propio objeto que recibe la asignacin, y la implementacin depender de las acciones necesarias para destruir el estado interno del objeto que recibe la asignacin y para asignar el estado interno del objeto recibido como parmetro al objeto que se est creando. Por ejemplo, para la clase Complejo:
class Complejo { public: Complejo& operator=(const Complejo& o) ; } ;

// Operador de Asignacin

y su implementacin podra ser la siguiente, que en este caso coincide con la implementacin que generara automaticamente el compilador en caso de que no fuese implementado por el programador:
Complejo& Complejo::operator=(const Complejo& o) { real = o.real ; imag = o.imag ; return *this ; } // Operador de Asignacion // Implementacin automtica

El operador de asignacin debe devolver el objeto actual (return *this) sobre el que recae la asignacin. Finalmente, a continuacin podemos ver un ejemplo de como sera una invocacin al operador de asignacin (para c3 y c4), junto a una invocacin a un constructor especco (para c2) y una invocacin al constructor por defecto (para c1), as como la asignacin (para c5 y c6) de objetos construidos invocando explcitamente a los constructores adecuados:
Dpto. Lenguajes y Ciencias de la Computacin Universidad de Mlaga

146

CAPTULO 11. TIPOS ABSTRACTOS DE DATOS


//- fichero: main.cpp ----------------------------------------------#include <iostream> #include "complejos.hpp" using namespace std ; using namespace umalcc ; real: 0.0 2.5 0.0 2.5 0.0 imag: 0.0 7.3 0.0 7.3 0.0 int main() c1 c2 c3 c4 c5 { Complejo c1, c3, c4, c5, c6; // Construccin por defecto de c1, c3, c4 Complejo c2(2.5, 7.3) ; // Construccin especfica c3 = c1 ; // Asignacin de c1 a c3 c4 = c2 ; // Asignacin de c2 a c4 c5 = Complejo(); // Asignacin de Complejo por Defecto c6 = Complejo(3.1, 4.2); // Asignacin de Complejo Especfico // ... } //- fin: main.cpp ---------------------------------------------------

3.1 4.2 c6

Hay situaciones en las que los objetos que se asignan tienen representaciones internas complejas, y en estos casos puede ser necesario destruir el estado interno del objeto que recibe la asignacin antes de asignar el nuevo valor. En este caso, es conveniente comprobar que no se est produciendo una auto-asignacin del mismo objeto (x = x), ya que en este caso se destruira la representacin interna del objeto antes de haberla asignado, con los errores que ello trae asociado. Por lo tanto, suele ser habitual que el operador de asignacin implemente una condicin para evitar la asignacin en el caso de que se produzca una auto-asignacin, de la siguiente forma:
Complejo& Complejo::operator=(const Complejo& o) // Operador de Asignacion { if (this != &o) { // destruir el valor anterior (en este caso no es necesario) real = o.real ; imag = o.imag ; } return *this ; }

As, this representa la direccin en memoria del objeto que recibe la asignacin, y &o representa la direccin en memoria del objeto que se recibe como parmetro. Si ambas direcciones son diferentes, entonces signica que son variables diferentes y se puede realizar la asignacin.

11.2.1.

Ejemplo

Veamos un Tipo Abstracto de Datos Lista de enteros, la cual permite almacenar una secuencia de nmero enteros, permitiendo insertar, eliminar, acceder y modicar elementos segn la posicin que ocupen en la secuencia de nmeros. Denicin
//- fichero: lista.hpp -----------------------------------------#ifndef _lista_hpp_ #define _lista_hpp_ #include <tr1/array> namespace umalcc { class ListaInt { public: //---------------------------------------------------------//-- Mtodos Pblicos -------------------------------------//---------------------------------------------------------// ~ListaInt() ; // Destructor Automtico //-----------------------------Dpto. Lenguajes y Ciencias de la Computacin Universidad de Mlaga

11.2. TIPOS ABSTRACTOS DE DATOS EN C++: MS SOBRE CLASES


ListaInt() ; ListaInt(const ListaInt& o) ; ListaInt& operator = (const ListaInt& o) ; //-----------------------------bool llena() const ; int size() const ; void clear() ; //-----------------------------void insertar(int pos, int dato) ; // PRECOND: ( ! llena() && pos >= 0 && pos <= size()) void eliminar(int pos) ; // PRECOND: (pos >= 0 && pos < size()) //-----------------------------int acceder(int pos) const ; // PRECOND: (pos >= 0 && pos < size()) void modificar(int pos, int dato); // PRECOND: (pos >= 0 && pos < size()) //---------------------------------------------------------private: //---------------------------------------------------------//-- Ctes y Tipos Privados --------------------------------//---------------------------------------------------------static const int MAX = 100; typedef std::tr1::array<int, MAX> Datos; //---------------------------------------------------------//-- Metodos Privados -------------------------------------//---------------------------------------------------------void abrir_hueco(int pos) ; void cerrar_hueco(int pos) ; //---------------------------------------------------------//-- Atributos Privados -----------------------------------//---------------------------------------------------------int sz; // numero de elementos de la lista Datos v; // contiene los elementos de la lista //---------------------------------------------------------}; } #endif //- fin: lista.hpp ----------------------------------------------

147

Implementacin
//- fichero: lista.cpp -----------------------------------------#include "lista.hpp" #include <cassert> namespace umalcc { //---------------------------------------------------------//-- Mtodos Pblicos -------------------------------------//---------------------------------------------------------// ListaInt::~ListaInt() { } // Destructor Automtico //---------------------------------ListaInt::ListaInt() : sz(0), v() { } // Constructor por Defecto //---------------------------------ListaInt::ListaInt(const ListaInt& o) // Constructor de Copia : sz(o.sz), v() { for (int i = 0; i < sz; ++i) { v[i] = o.v[i] ; }
Dpto. Lenguajes y Ciencias de la Computacin Universidad de Mlaga

148

CAPTULO 11. TIPOS ABSTRACTOS DE DATOS


} //---------------------------------ListaInt& ListaInt::operator = (const ListaInt& o) // Op. de Asignacin { if (this != &o) { sz = o.sz ; for (int i = 0; i < sz; ++i) { v[i] = o.v[i] ; } } return *this ; } //---------------------------------bool ListaInt::llena() const { return sz == int(v.size()); } //---------------------------------int ListaInt::size() const { return sz ; } //---------------------------------void ListaInt::clear() { sz = 0 ; } //---------------------------------void ListaInt::insertar(int pos, int dato) { assert( ! llena() && pos >= 0 && pos <= size()) ; abrir_hueco(pos) ; v[pos] = dato ; } //---------------------------------void ListaInt::eliminar(int pos) { assert(pos >= 0 && pos < size()) ; cerrar_hueco(pos) ; } //---------------------------------int ListaInt::acceder(int pos) const { assert(pos >= 0 && pos < size()) ; return v[pos] ; } //---------------------------------void ListaInt::modificar(int pos, int dato) { assert(pos >= 0 && pos < size()) ; v[pos] = dato; } //---------------------------------------------------------//-- Metodos Privados -------------------------------------//---------------------------------------------------------void ListaInt::abrir_hueco(int pos) { assert(sz < int(v.size())) ; for (int i = sz; i > pos; --i) {

Dpto. Lenguajes y Ciencias de la Computacin

Universidad de Mlaga

11.2. TIPOS ABSTRACTOS DE DATOS EN C++: MS SOBRE CLASES


v[i] = v[i-1]; } ++sz; } //---------------------------------void ListaInt::cerrar_hueco(int pos) { assert(sz > 0) ; --sz; for (int i = pos; i < sz; ++i) { v[i] = v[i+1]; } } //----------------------------------

149

// Ahora hay un elemento ms

// Ahora hay un elemento menos

} //- fin: lista.cpp ----------------------------------------------

Utilizacin
//- fichero: main.cpp ----------------------------------------------#include <iostream> #include <cctype> #include <cassert> #include "lista.hpp" using namespace std ; using namespace umalcc ; //-----------------------------------------------------------------void leer_pos(int& pos, int limite) { assert(limite > 0); do { cout << "Introduzca posicion ( < " << limite << " ): " ; cin >> pos; } while (pos < 0 || pos >= limite); } //--------------------------------void leer_dato(int& dato) { cout << "Introduzca un dato: " ; cin >> dato; } //--------------------------------void leer(ListaInt& lista) { int dato ; lista.clear() ; cout << "Introduzca datos (0 -> FIN): " << endl ; cin >> dato ; while ((dato != 0)&&( ! lista.llena())) { lista.insertar(lista.size(), dato) ; cin >> dato ; } } //--------------------------------void escribir(const ListaInt& lista) { cout << "Lista: " ; for (int i = 0 ; i < lista.size() ; ++i) { cout << lista.acceder(i) << " " ;
Dpto. Lenguajes y Ciencias de la Computacin Universidad de Mlaga

150
} cout << endl ;

CAPTULO 11. TIPOS ABSTRACTOS DE DATOS

} //--------------------------------void prueba_asg(const ListaInt& lista) { cout << "Constructor de Copia" << endl ; ListaInt lst(lista) ; escribir(lst) ; cout << "Operador de Asignacion" << endl ; lst = lista ; escribir(lst) ; } //------------------------------------------------------------------------char menu() { char op ; cout << endl ; cout << "X. Fin" << endl ; cout << "A. Leer Lista" << endl ; cout << "B. Borrar Lista" << endl ; cout << "C. Insertar Posicion" << endl ; cout << "D. Eliminar Posicion" << endl ; cout << "E. Acceder Posicion" << endl ; cout << "F. Modificar Posicion" << endl ; cout << "G. Prueba Copia y Asignacion" << endl ; do { cout << endl << " Opcion: " ; cin >> op ; op = char(toupper(op)) ; } while (!((op == X)||((op >= A)&&(op <= G)))) ; cout << endl ; return op ; } //------------------------------------------------------------------------int main() { ListaInt lista ; int dato ; int pos ; char op = ; do { op = menu() ; switch (op) { case A: leer(lista) ; escribir(lista) ; break ; case B: lista.clear() ; escribir(lista) ; break ; case C: if (lista.llena()) { cout << "Error: Lista llena" << endl ; } else { leer_pos(pos, lista.size()+1) ; leer_dato(dato) ; lista.insertar(pos, dato) ;

Dpto. Lenguajes y Ciencias de la Computacin

Universidad de Mlaga

11.2. TIPOS ABSTRACTOS DE DATOS EN C++: MS SOBRE CLASES


escribir(lista) ; } break ; case D: if (lista.size() == 0) { cout << "Error: lista vacia" << endl ; } else { leer_pos(pos, lista.size()) ; lista.eliminar(pos) ; escribir(lista) ; } break ; case E: if (lista.size() == 0) { cout << "Error: lista vacia" << endl ; } else { leer_pos(pos, lista.size()) ; cout << "Lista[" << pos << "]: " << lista.acceder(pos) << endl ; escribir(lista) ; } break ; case F: if (lista.size() == 0) { cout << "Error: lista vacia" << endl ; } else { leer_pos(pos, lista.size()) ; leer_dato(dato) ; lista.modificar(pos, dato) ; escribir(lista) ; } break ; case G: prueba_asg(lista) ; break ; } } while (op != X) ; } //- fin: main.cpp ---------------------------------------------------

151

Dpto. Lenguajes y Ciencias de la Computacin

Universidad de Mlaga

152

CAPTULO 11. TIPOS ABSTRACTOS DE DATOS

Dpto. Lenguajes y Ciencias de la Computacin

Universidad de Mlaga

Captulo 12

Introduccin a la Programacin Genrica. Plantillas


El lenguaje de programacin C++ proporciona soporte a la programacin genrica mediante las plantillas (templates en ingls). Las plantillas proporcionan un mecanismo ecaz para denir cdigo (constantes, tipos y subprogramas) genricos parametrizados, que puedan ser instanciados en tiempo de compilacin . Estos parmetros genricos de las plantillas podrn ser instanciados con tipos y valores constantes concretos especicados en tiempo de compilacin. Las deniciones genricas debern, por lo general, estar visibles en el lugar donde sean instanciadas, por lo que en el caso de denirse en mdulos diferentes, debern estar denidas en los cheros de encabezamiento, para que puedan ser incluidas por todos aquellos mdulos que las necesiten. La denicin de plantillas, tanto de subprogramas como de tipos comienza con la palabra reservada template, seguida entre delimitadores < ... > por los parmetros genricos de la denicin. Estos parmetros genricos pueden ser tanto tipos (precedidos por la palabra reservada typename), como constantes de tipos integrales (char, short, int, unsigned, long) o de tipos genricos parametrizados con anterioridad (que deben ser instanciados a tipos integrales).

12.1.

Subprogramas Genricos

Los subprogramas genricos son tiles cuando denen procesamientos genricos que son independientes de los tipos concretos sobre los que se aplican. En este caso, simplemente se dene el subprograma utilizando tanto los tipos como constantes genricas mediante sus identicadores declarados en la directiva template <...>. Posteriormente, cuando se utilicen estos subprogramas genricos, tanto los tipos como constantes genricas sern instanciadas segn los tipos y constantes actuales utilizados en la invocacin a dichos subprogramas. Veamos algunos ejemplos de denicin de subprogramas genricos:
template <typename Tipo> inline Tipo maximo(const Tipo& x, const Tipo& y) { Tipo max; if (x > y) { max = x ; } else { max = y ; } return max ; }

153

154 CAPTULO 12. INTRODUCCIN A LA PROGRAMACIN GENRICA. PLANTILLAS


template <typename Tipo> inline void intercambio(Tipo& x, Tipo& y) { Tipo aux = x ; x = y ; y = aux ; } int main() { int x = 4 ; int y = maximo(x, 8) ; intercambio(x, y) ; double a = 7.5 ; double b = maximo(a, 12.0) ; intercambio(a, b) ; double c = maximo(a, 12) ; // Error: maximo(double, int) no esta definido }

En el ejemplo se puede ver que los parmetros de entrada a los subprogramas se pasan por referencia (constante o variable), ya que al ser un tipo genrico podra ser tanto un tipo simple como un tipo estructurado. Tambin puede apreciarse que la instanciacin de subprogramas genricos a tipos concretos se realiza automticamente a partir de la invocacin a los mismos, de tal forma que la instanciacin de los parmetros genricos se realiza por deduccin a partir del tipo de los parmetros especicados en la invocacin a los subprogramas. Sin embargo, hay situaciones donde los parmetros genricos no pueden ser deducidos de la propia invocacin al subprograma. En este caso, los parmetros genricos involucrados deben ser especicados explcitamente en la llamada.
int main() { double a = 7.5 ; double b = maximo(a, 12.0) ; double c = maximo<double>(a, 12) ; }

El siguiente ejemplo de subprograma genrico muestra la utilizacin de parmetros genricos constantes (de tipo integral).
#include <iostream> #include <string> #include <tr1/array> using namespace std ; using namespace std::tr1 ; //------------------------------------template <typename TipoBase, unsigned SIZE> void asignar(array<TipoBase, SIZE>& v, const TipoBase& x) { for (int i = 0 ; i < int(v.size()) ; ++i) { v[i] = x ; } } //------------------------------------// Requiere que el TipoBase se pueda escribir con << template <typename TipoBase, unsigned SIZE> void escribir(const array<TipoBase, SIZE>& v)
Dpto. Lenguajes y Ciencias de la Computacin Universidad de Mlaga

12.1. SUBPROGRAMAS GENRICOS


{ for (int i = 0 ; i < int(v.size()) ; ++i) { cout << v[i] << " "; } cout << endl ; } //------------------------------------typedef array<int, 5> AInt ; void prueba1() { AInt a = {{ 1, 2, 3, 4, 5 }}; escribir(a); asignar(a, 5) ; escribir(a); } //------------------------------------typedef array<string, 3> APers ; void prueba2() { APers a = {{ "pepe", "juan", "mara" }} ; escribir(a); asignar(a, "lola") ; escribir(a); } //-------------------------------------

155

Errores de Instanciacin de Parmetros Genricos En el caso de que la denicin de un determinado subprograma sea incorrecta para una determinada instanciacin concreta de los parmetros genricos, se producir un error de compilacin indicando el tipo de error. Por ejemplo, si compilamos el siguiente cdigo, se produce un error de compilacin, ya que el tipo Persona no tiene denido el operador de salida (<<).
#include <iostream> #include <string> #include <tr1/array> using namespace std ; using namespace std::tr1 ; //------------------------------------// Requiere que el TipoBase se pueda escribir con << template <typename TipoBase, unsigned SIZE> void escribir(const array<TipoBase, SIZE>& v) { for (int i = 0 ; i < int(v.size()) ; ++i) { cout << v[i] << " "; // lnea 12 } cout << endl ; } //------------------------------------struct Persona { string nombre ; string telefono ; } ; typedef array<Persona, 4> APersona ; void prueba3() { APersona a = {{ { "pepe", "00" }, { "juan", "11" },
Dpto. Lenguajes y Ciencias de la Computacin Universidad de Mlaga

156 CAPTULO 12. INTRODUCCIN A LA PROGRAMACIN GENRICA. PLANTILLAS


{ "mara", "22" }, { "carmen", "33" } }} ; escribir(a); } //-------------------------------------

// lnea 30

Produce el siguiente mensaje de error de compilacin (con GNU GCC ):


main.cpp: In function void escribir(const std::tr1::array<_Tp, _Nm>&) [with TipoBase = Persona, unsigned int SIZE = 4u]: main.cpp:30: instantiated from here main.cpp:12: error: no match for operator<< in ... [......]

Cuando se trabaja con plantillas en C++, hay que tener presente que, a veces, los mensajes de error pueden ser bastante complicados de interpretar en el caso de errores producidos por instanciaciones de parmetros genricos. En estos casos, el primer mensaje de error suele ser el ms til para guiarnos en su correccin.

12.2.

Tipos Abstractos de Datos Genricos

Las plantillas tambin pueden ser utilizadas para la denicin de tipos genricos, tanto registros como clases genricas (TADs genricos). Como se explic al comienzo del captulo, las deniciones genricas deben, por lo general, estar visibles en el lugar donde sean instanciadas, y por lo tanto, en el caso de tipos abstractos genricos tanto la denicin como la implementacin se realizar completamente dentro de los cheros de encabezamiento. Adems, en esta seccin introductoria a la programacin genrica slo veremos los tipos abstractos de datos genricos denidos e implementados en lnea , y en su versin ms simple. La denicin de una clase genrica se realiza de forma similar a la denicin de una clase no genrica, pero con algunas diferencias: Se precede la denicin de la clase con la palabra reservada template seguida entre delimitadores < ... > por los parmetros genricos de la denicin. Estos parmetros genricos pueden ser tanto tipos (precedidos por la palabra reservada typename), como constantes de tipos integrales (char, short, int, unsigned, long) o de tipos genricos parametrizados con anterioridad (que deben ser instanciados a tipos integrales). Los tipos y constantes genricas de la plantilla pueden ser utilizados en la denicin de la clase mediante sus identicadores declarados en la directiva template <...>. La implementacin de los mtodos se debe realizar en-lnea , es decir, el cuerpo del mtodo tambin se denir dentro de la denicin de la clase, a diferencia de como se ha visto hasta ahora, en la cual los mtodos se implementaban de forma independiente en un chero de implementacin aparte. En la instanciacin de tipos abstractos genricos, ser necesaria la instanciacin explcita de los parmetros de los mismos, a diferencia de la instanciacin de subprogramas genricos vista anteriormente, en la cual los parmetros genricos se instancian automticamente a travs de la invocacin a los subprogramas. Veamos algunos ejemplos de denicin y utilizacin de tipos abstractos genricos. Ejemplo 1 Consideremos el tipo abstracto genrico denominado Par, que almacena dos elementos de tipos genricos, y proporciona mtodos tanto para conocer sus valores, como para su modicacin.
Dpto. Lenguajes y Ciencias de la Computacin Universidad de Mlaga

12.2. TIPOS ABSTRACTOS DE DATOS GENRICOS


//- par.hpp ------------------------------------------------------------#ifndef _par_hpp_ #define _par_hpp_ namespace umalcc { template <typename Tipo_1, typename Tipo_2> class Par { public: //-----------------------------// Destructor Automtico // ~Par() { } //-----------------------------// Constructor Copia Automtico // Par(const Par& o) : elem_1(o.elem_1), elem_2(o.elem_2) { } //-----------------------------// Operador Asignacin Automtico // Par& operator = (const Par& o) // { // if (this != &o) { // elem_1 = o.elem_1; // elem_2 = o.elem_2; // } // return *this; // } //-----------------------------Par() : elem_1(), elem_2() { } //-----------------------------Par(const Tipo_1& valor_1, const Tipo_2& valor_2) : elem_1(valor_1), elem_2(valor_2) { } //-----------------------------Tipo_1 primero() const { // Devuelve el valor del primer elemento del par return elem_1; } //-----------------------------Tipo_2 segundo() const { // Devuelve el valor del segundo elemento del par return elem_2; } //-----------------------------void asg_primero(const Tipo_1& valor) { // Asigna un valor al primer elemento del par elem_1 = valor; } //-----------------------------void asg_segundo(const Tipo_2& valor) { // Asigna un valor al segundo elemento del par elem_2 = valor; } //-----------------------------private: Tipo_1 elem_1; Tipo_2 elem_2; }; } #endif

157

Dpto. Lenguajes y Ciencias de la Computacin

Universidad de Mlaga

158 CAPTULO 12. INTRODUCCIN A LA PROGRAMACIN GENRICA. PLANTILLAS A continuacin podemos ver un ejemplo de su utilizacin:
//- main.hpp ------------------------------------------------------------#include <iostream> #include <string> #include "par.hpp" using namespace std; using namespace umalcc; //-------------------------------------typedef Par<int, int> ParInt; typedef Par<string, int> ParPer; //-------------------------------------int main() { ParInt x(3, 7) ; // Crea un Par de enteros con los valores 3 y 7 ParPer p("pepe", 23) ; // Crea un Par de Persona con los valores "pepe" y 23 ParPer q = p; // Copia el par de p a q cout << x.primero() << " " << x.segundo() << endl; p.asg_primero("juan"); cout << p.primero() << " " << p.segundo() << endl; } //--------------------------------------

Ejemplo 2 En el captulo anterior (vase 11.2.1) se vio un ejemplo de un TAD Lista de nmero enteros. Denicin e Implementacin Podemos denir una TAD genrico Lista que permita almacenar elementos homogneos de un tipo de datos genrico, y donde su capacidad mxima tambin est parametrizada de forma genrica:
//- fichero: lista.hpp -----------------------------------------#ifndef _lista_hpp_ #define _lista_hpp_ #include <tr1/array> #include <cassert> namespace umalcc { template <typename TipoBase, unsigned SIZE> class Lista { public: //---------------------------------------------------------//-- Mtodos Pblicos -------------------------------------//---------------------------------------------------------// ~Lista() { } // Destructor Automtico //-----------------------------Lista() : sz(0), v() { } // Constructor por Defecto //-----------------------------Lista(const Lista& o) // Constructor de Copia : sz(o.sz), v() { for (int i = 0; i < sz; ++i) { v[i] = o.v[i] ; } } //-----------------------------Lista& operator = (const Lista& o) // Operador de Asignacin { if (this != &o) {
Dpto. Lenguajes y Ciencias de la Computacin Universidad de Mlaga

12.2. TIPOS ABSTRACTOS DE DATOS GENRICOS


sz = o.sz ; for (int i = 0; i < sz; ++i) { v[i] = o.v[i] ; } } return *this ; } //-----------------------------bool llena() const { return sz == int(v.size()); } //-----------------------------int size() const { return sz ; } //-----------------------------void clear() { sz = 0 ; } //-----------------------------// PRECOND: ( ! llena() && pos >= 0 && pos <= size()) void insertar(int pos, const TipoBase& dato) { assert( ! llena() && pos >= 0 && pos <= size()) ; abrir_hueco(pos) ; v[pos] = dato ; } //-----------------------------// PRECOND: (pos >= 0 && pos < size()) void eliminar(int pos) { assert(pos >= 0 && pos < size()) ; cerrar_hueco(pos) ; } //-----------------------------// PRECOND: (pos >= 0 && pos < size()) TipoBase acceder(int pos) const { assert(pos >= 0 && pos < size()) ; return v[pos] ; } //-----------------------------// PRECOND: (pos >= 0 && pos < size()) void modificar(int pos, const TipoBase& dato) { assert(pos >= 0 && pos < size()) ; v[pos] = dato; } //---------------------------------------------------------private: //---------------------------------------------------------//-- Ctes y Tipos Privados --------------------------------//---------------------------------------------------------typedef std::tr1::array<TipoBase, SIZE> Datos; //---------------------------------------------------------//-- Metodos Privados --------------------------------------

159

Dpto. Lenguajes y Ciencias de la Computacin

Universidad de Mlaga

160 CAPTULO 12. INTRODUCCIN A LA PROGRAMACIN GENRICA. PLANTILLAS


//---------------------------------------------------------void abrir_hueco(int pos) { assert(sz < int(v.size())) ; for (int i = sz; i > pos; --i) { v[i] = v[i-1]; } ++sz; // Ahora hay un elemento ms } //-----------------------------void cerrar_hueco(int pos) { assert(sz > 0) ; --sz; // Ahora hay un elemento menos for (int i = pos; i < sz; ++i) { v[i] = v[i+1]; } } //---------------------------------------------------------//-- Atributos Privados -----------------------------------//---------------------------------------------------------int sz; // numero de elementos de la lista Datos v; // contiene los elementos de la lista //---------------------------------------------------------}; } #endif //- fin: lista.hpp ----------------------------------------------

Utilizacin Veamos a continuacin un ejemplo de instanciacin y utilizacin del TAD Lista genrica visto anteriormente:
//- fichero: main.cpp ----------------------------------------------#include <iostream> #include <cctype> #include <cassert> #include "lista.hpp" using namespace std ; using namespace umalcc ; //-----------------------------------------------------------------// Instanciacin de la Lista genrica para almacenar hasta 100 nmeros enteros typedef Lista<int, 100> ListaInt; //-----------------------------------------------------------------void leer_pos(int& pos, int limite) { assert(limite > 0); do { cout << "Introduzca posicion ( < " << limite << " ): " ; cin >> pos; } while (pos < 0 || pos >= limite); } //--------------------------------void leer_dato(int& dato) { cout << "Introduzca un dato: " ; cin >> dato; } //--------------------------------Dpto. Lenguajes y Ciencias de la Computacin Universidad de Mlaga

12.2. TIPOS ABSTRACTOS DE DATOS GENRICOS


void leer(ListaInt& lista) { int dato ; lista.clear() ; cout << "Introduzca datos (0 -> FIN): " << endl ; cin >> dato ; while ((dato != 0)&&( ! lista.llena())) { lista.insertar(lista.size(), dato) ; cin >> dato ; } } //--------------------------------void escribir(const ListaInt& lista) { cout << "Lista: " ; for (int i = 0 ; i < lista.size() ; ++i) { cout << lista.acceder(i) << " " ; } cout << endl ; } //--------------------------------void prueba_asg(const ListaInt& lista) { cout << "Constructor de Copia" << endl ; ListaInt lst(lista) ; escribir(lst) ; cout << "Operador de Asignacion" << endl ; lst = lista ; escribir(lst) ; } //------------------------------------------------------------------------char menu() { char op ; cout << endl ; cout << "X. Fin" << endl ; cout << "A. Leer Lista" << endl ; cout << "B. Borrar Lista" << endl ; cout << "C. Insertar Posicion" << endl ; cout << "D. Eliminar Posicion" << endl ; cout << "E. Acceder Posicion" << endl ; cout << "F. Modificar Posicion" << endl ; cout << "G. Prueba Copia y Asignacion" << endl ; do { cout << endl << " Opcion: " ; cin >> op ; op = char(toupper(op)) ; } while (!((op == X)||((op >= A)&&(op <= G)))) ; cout << endl ; return op ; } //------------------------------------------------------------------------int main() { ListaInt lista ; int dato ; int pos ; bool ok ; char op = ;

161

Dpto. Lenguajes y Ciencias de la Computacin

Universidad de Mlaga

162 CAPTULO 12. INTRODUCCIN A LA PROGRAMACIN GENRICA. PLANTILLAS


do { op = menu() ; switch (op) { case A: leer(lista) ; escribir(lista) ; break ; case B: lista.clear() ; escribir(lista) ; break ; case C: if (lista.llena()) { cout << "Error: Lista llena" << endl ; } else { leer_pos(pos, lista.size()+1) ; leer_dato(dato) ; lista.insertar(pos, dato) ; escribir(lista) ; } break ; case D: if (lista.size() == 0) { cout << "Error: lista vacia" << endl ; } else { leer_pos(pos, lista.size()) ; lista.eliminar(pos) ; escribir(lista) ; } break ; case E: if (lista.size() == 0) { cout << "Error: lista vacia" << endl ; } else { leer_pos(pos, lista.size()) ; cout << "Lista[" << pos << "]: " << lista.acceder(pos) << endl ; escribir(lista) ; } break ; case F: if (lista.size() == 0) { cout << "Error: lista vacia" << endl ; } else { leer_pos(pos, lista.size()) ; leer_dato(dato) ; lista.modificar(pos, dato) ; escribir(lista) ; } break ; case G: prueba_asg(lista) ; break ; } } while (op != X) ; } //- fin: main.cpp ---------------------------------------------------

Dpto. Lenguajes y Ciencias de la Computacin

Universidad de Mlaga

Captulo 13

Memoria Dinmica. Punteros


Hasta ahora, todos los programas que se han visto en captulos anteriores almacenan su estado interno por medio de variables que son automticamente gestionadas por el compilador. Las variables son creadas cuando el ujo de ejecucin entra en el mbito de su denicin (se reserva espacio en memoria y se crea el valor de su estado inicial), posteriormente se manipula el estado de la variable (accediendo o modicando su valor almacenado), y nalmente se destruye la variable cuando el ujo de ejecucin sale del mbito donde fue declarada la variable (liberando los recursos asociados a ella y la zona de memoria utilizada). A este tipo de variables gestionadas automticamente por el compilador se las suele denominar variables automticas (tambin variables locales), y residen en una zona de memoria gestionada automticamente por el compilador, la pila de ejecucin, donde se alojan y desalojan las variables locales (automticas) pertenecientes al mbito de ejecucin de cada subprograma. As, el tiempo de vida de una determinada variable est condicionado por el mbito de su declaracin. Adems, el nmero de variables automticas utilizadas en un determinado programa est especicado explcitamente en el propio programa, y por lo tanto su capacidad de almacenamiento est tambin especicada y predeterminada por lo especicado explcitamente en el programa. Es decir, con la utilizacin nica de variables automticas, la capacidad de almacenamiento de un determinado programa est predeterminada desde el momento de su programacin (tiempo de compilacin), y no puede adaptarse a las necesidades reales de almacenamiento surgidas durante la ejecucin del programa (tiempo de ejecucin).1 La gestin de memoria dinmica surge como un mecanismo para que el propio programa, durante su ejecucin (tiempo de ejecucin), pueda solicitar (alojar) y liberar (desalojar) memoria segn las necesidades surgidas durante una determinada ejecucin, dependiendo de las circunstancias reales de cada momento de la ejecucin del programa en un determinado entorno. Esta ventaja adicional viene acompaada por un determinado coste asociado a la mayor complejidad que requiere su gestin, ya que en el caso de las variables automticas, es el propio compilador el encargado de su gestin, sin embargo en el caso de las variables dinmicas es el propio programador el que debe, mediante cdigo software, gestionar el tiempo de vida de cada variable dinmica, cuando debe ser alojada y creada, como ser utilizada, y nalmente cuando debe ser destruida y desalojada. Adicionalmente, como parte de esta gestin de la memoria dinmica por el propio programador, la memoria dinmica pasa a ser un recurso que debe gestionar el programador, y se debe preocupar de su alojo y de su liberacin, poniendo especial cuidado y nfasis en no perder recursos (perder zonas de memoria sin liberar y sin capacidad de acceso).

1 En realidad esto no es completamente cierto, ya que en el caso de subprogramas recursivos, cada invocacin recursiva en tiempo de ejecucin tiene la capacidad de alojar nuevas variables que sern posteriormente desalojadas automticamente cuando la llamada recursiva naliza.

163

164

CAPTULO 13. MEMORIA DINMICA. PUNTEROS

13.1.

Punteros

El tipo puntero es un tipo simple que permite a un determinado programa acceder a posiciones concretas de memoria, y ms especcamente a determinadas zonas de la memoria dinmica. Aunque el lenguaje de programacin C++ permite otras utilizaciones ms diversas del tipo puntero, en este captulo slo se utilizar el tipo puntero para acceder a zonas de memoria dinmica. As, una determinada variable de tipo puntero apunta (o referencia) a una determinada entidad (variable) de un determinado tipo alojada en la zona de memoria dinmica. Por lo tanto, para un determinado tipo puntero, se debe especicar tambin el tipo de la variable (en memoria dinmica) a la que apunta, el cual dene el espacio que ocupa en memoria y las operaciones (y mtodos) que se le pueden aplicar, entre otras cosas. De este modo, cuando un programa gestiona la memoria dinmica a travs de punteros, debe manejar y gestionar por una parte la propia variable de tipo puntero, y por otra parte la variable dinmica apuntada por ste. Un tipo puntero se dene utilizando la palabra reservada typedef seguida del tipo de la variable dinmica apuntada, un asterisco para indicar que es un puntero a una variable de dicho tipo, y el identicador que denomina al tipo. Por ejemplo:
typedef int* PInt ; // Tipo Puntero a Entero

struct Persona { // Tipo Persona string nombre ; string telefono ; int edad ; } ; typedef Persona* PPersona ; // Tipo Puntero a Persona

As, el tipo PInt es el tipo de una variable que apunta a una variable dinmica de tipo int. Del mismo modo, el tipo PPersona es el tipo de una variable que apunta a una variable dinmica de tipo Persona. Es importante remarcar que el tipo puntero, en s mismo, es un tipo simple, aunque el tipo apuntado puede ser tanto un tipo simple, como un tipo compuesto. Es posible denir variables de los tipos especicados anteriormente. Ntese que estas variables (p1 y p2 en el siguiente ejemplo) son variables automticas (gestionadas automticamente por el compilador), es decir, se crean automticamente (con un valor indeterminado) al entrar el ujo de ejecucin en el mbito de visibilidad de la variable, y posteriormente se destruyen automticamente cuando el ujo de ejecucin sale del mbito de visibilidad de la variable. Por otra parte, las variables apuntadas por ellos son variables dinmicas (gestionadas por el programador), es decir el programador se encargar de solicitar la memoria dinmica cuando sea necesaria y de liberarla cuando ya no sea necesaria, durante la ejecucin del programa. En el siguiente ejemplo, si las variables se denen sin inicializar, entonces tendrn un valor inicial inespecicado:
int main() { PInt p1 ; PPersona p2 ; } p1: p2: ? ?

La constante NULL es una constante especial de tipo puntero que indica que una determinada variable de tipo puntero no apunta a nada, es decir, especica que la variable de tipo puntero que contenga el valor NULL no apunta a ninguna zona de la memoria dinmica. Para utilizar la constante NULL se debe incluir la biblioteca estndar <cstddef>. As, se pueden denir las variables p1 y p2 e inicializarlas a un valor indicando que no apuntan a nada.
#include <cstddef> int main() { PInt p1 = NULL ; PPersona p2 = NULL ; } p1: p2:

Dpto. Lenguajes y Ciencias de la Computacin

Universidad de Mlaga

13.2. GESTIN DE MEMORIA DINMICA

165

13.2.

Gestin de Memoria Dinmica

La memoria dinmica la debe gestionar el propio programador, por lo que cuando necesite crear una determinada variable dinmica, debe solicitar memoria dinmica con el operador new seguido por el tipo de la variable dinmica a crear. Este operador (new) realiza dos acciones principales, primero aloja (reserva) espacio en memoria dinmica para albergar a la variable, y despus crea (invocando al constructor especicado) el contenido de la variable dinmica. Finalmente, a la variable ptr se le asigna el valor del puntero (una direccin de memoria) que apunta a la variable dinmica creada por el operador new. Por ejemplo, para crear una variable dinmica del tipo Persona denido anteriormente utilizando el constructor por defecto de dicho tipo.
int main() { PPersona ptr ; ptr = new Persona ; } ptr: ???

En caso de que el tipo de la variable dinmica tenga otros constructores denidos, es posible utilizarlos en la construccin del objeto en memoria dinmica. Por ejemplo, suponiendo que el tipo Persona tuviese un constructor que reciba el nombre, telfono y edad de la persona:
int main() { PPersona ptr ; ptr = new Persona("pepe", "111", 5) ; } ptr: pepe 111 5

Posteriormente, tras manipular adecuadamente, segn las caractersticas del programa, la memoria dinmica alojada, llegar un momento en que dicha variable dinmica ya no sea necesaria, y su tiempo de vida llegue a su n. En este caso, el programador debe liberar explcitamente dicha variable dinmica mediante el operador delete de la siguiente forma:
ptr: int main() { PPersona ptr ; // Creacin automtica de la variable PTR ptr = new Persona ; // Creacin de la variable dinmica annima ptr: // manipulacin ... delete ptr ; } // Destruccin de la variable dinmica annima // Destruccin automtica de la variable PTR pepe 111 5

liberada

La sentencia delete ptr realiza dos acciones principales, primero destruye la variable dinmica (invocando a su destructor), y despus desaloja (libera) la memoria dinmica reservada para dicha variable. Finalmente la variable local ptr queda con un valor inespecicado, y ser destruida automticamente por el compilador cuando el ujo de ejecucin salga de su mbito de declaracin. Si se ejecuta la operacin delete sobre una variable de tipo puntero que tiene el valor NULL, entonces esta operacin no hace nada. En caso de que no se libere (mediante el operador delete) la memoria dinmica apuntada por la variable ptr, y esta variable sea destruida al terminar su tiempo de vida (su mbito de visibilidad), entonces se perder la memoria dinmica a la que apunta, con la consiguiente prdida de recursos que ello conlleva.
int main() { PPersona ptr ; ptr = new Persona("pepe", "111", 5) ; // manipulacin ... // no se libera la memoria dinmica apuntada por ptr // se destruye la variable local ptr }
Dpto. Lenguajes y Ciencias de la Computacin Universidad de Mlaga

ptr:

pepe 111 5

perdida
pepe 111 5

166

CAPTULO 13. MEMORIA DINMICA. PUNTEROS

13.3.

Operaciones con Variables de Tipo Puntero

Asignacin de Variables de Tipo Puntero


El puntero nulo (NULL) se puede asignar a cualquier variable de tipo puntero. Por ejemplo:
int main() { PPersona p1 ; // ... p1 = NULL ; // ... }

p1: p1:

El resultado de crear una variable dinmica con el operador new se puede asignar a una variable de tipo puntero al tipo de la variable dinmica creada. Por ejemplo:
int main() { PPersona p1 ; // ... p1 = new Persona("pepe", "111", 5) ; // ... } p1: p1: ? pepe 111 5

As mismo, a una variable de tipo puntero se le puede asignar el valor de otra variable puntero. En este caso, ambas variables de tipo puntero apuntarn a la misma variable dinmica, que ser compartida por ambas. Si se libera la variable dinmica apuntada por una de ellas, la variable dinmica compartida se destruye, su memoria se desaloja y ambas variables locales de tipo puntero quedan con un valor inespecicado.
p1: int main() p2: { PPersona p1 = new Persona("pepe", "111", 5) ; PPersona p2 ; // ... p1: p2 = p1 ; p2: // ... delete p1 ; } pepe 111 5

? ?

liberada

En la operacin de asignacin, el valor anterior que tuviese la variable de tipo puntero se pierde, por lo que habr que tener especial cuidado de que no se pierda la variable dinmica que tuviese asignada, si tuviese alguna.
p1: int main() { PPersona p1 = new Persona("pepe", "111", 5) ; // ... p1 = NULL ; // se pierde el valor anterior p1: // ... delete p1 ; } pepe 111 5

perdida
pepe 111 5

Desreferenciacin de una Variable de Tipo Puntero


Para acceder a una variable dinmica apuntada por una variable de tipo puntero, se utiliza el operador unario asterisco (*) precediendo al nombre de la variable de tipo puntero a travs de la cual es apuntada. Por ejemplo, si ptr es una variable local de tipo puntero que apunta a una variable dinmica de tipo Persona, entonces *ptr es la variable dinmica apuntada, y se trata de igual forma que cualquier otra variable de tipo Persona.
Dpto. Lenguajes y Ciencias de la Computacin Universidad de Mlaga

13.3. OPERACIONES CON VARIABLES DE TIPO PUNTERO


int main() { PPersona ptr = new Persona("pepe", "111", 5) ; Persona p = *ptr ; *ptr = p ; delete ptr ; }

167

// Asigna el contenido de la variable dinmica a la variable p // Asigna el contenido de la variable p a la variable dinmica // destruye la variable dinmica y libera su espacio de memoria

Sin embargo, si una variable de tipo puntero tiene el valor NULL, entonces desreferenciar la variable produce un error en tiempo de ejecucin que aborta la ejecucin del programa. As mismo, desreferenciar un puntero con valor inespecicado produce un comportamiento anmalo en tiempo de ejecucin. Es posible, as mismo, acceder a los elementos de la variable apuntada mediante el operador de desreferenciacin. Por ejemplo:
int main() { PPersona ptr = new Persona ; (*ptr).nombre = "pepe" ; (*ptr).telefono = "111" ; (*ptr).edad = 5 ; delete ptr ; }

Ntese que el uso de los parntesis es obligatorio debido a que el operador punto (.) tiene mayor precedencia que el operador de desreferenciacin (*). Por ello, en el caso de acceder a los campos de un registro en memoria dinmica a travs de una variable de tipo puntero, es ms adecuado utilizar el operador de desreferenciacin (->). Por ejemplo:
int main() { PPersona ptr = new Persona ; ptr->nombre = "pepe" ; ptr->telefono = "111" ; ptr->edad = 5 ; delete ptr ; }

Este operador tambin se utiliza para invocar a mtodos de un objeto si ste se encuentra alojado en memoria dinmica. Por ejemplo:
#include <iostream> using namespace std ; class Numero { public: Numero(int v) : val(v) {} int valor() const { return val ; } private: int val ; } ; typedef Numero* PNumero ; int main() {
Dpto. Lenguajes y Ciencias de la Computacin Universidad de Mlaga

168

CAPTULO 13. MEMORIA DINMICA. PUNTEROS


PNumero ptr = new Numero(5) ; cout << ptr->valor() << endl ; delete ptr ; }

Comparacin de Variables de Tipo Puntero


Las variables del mismo tipo puntero se pueden comparar entre ellas por igualdad (==) o desigualdad (!=), para comprobar si apuntan a la misma variable dinmica. As mismo, tambin se pueden comparar por igualdad o desigualdad con el puntero nulo (NULL) para saber si apunta a alguna variable dinmica, o por el contrario no apunta a nada. Por ejemplo:
int main() { PPersona p1, p2 ; // ... if (p1 == p2) { // ... } if (p1 != NULL) { // ... } }

13.4.

Paso de Parmetros de Variables de Tipo Puntero

El tipo puntero es un tipo simple, y por lo tanto se tratar como tal. En caso de paso de parmetros de tipo puntero, si es un parmetro de entrada, entonces se utilizar el paso por valor, y si es un parmetro de salida o de entrada/salida, entonces se utilizar el paso por referencia. Hay que ser consciente de que un parmetro de tipo puntero puede apuntar a una variable dinmica, y en este caso, a partir del parmetro se puede acceder a la variable apuntada. As, si el parmetro se pasa por valor, entonces se copia el valor del puntero del parmetro actual (en la invocacin) al parmetro formal (en el subprograma), por lo que ambos apuntarn a la misma variable dinmica compartida, y en este caso, si se modica el valor almacenado en la variable dinmica, este valor se ver afectado, as mismo, en el exterior del subprograma, aunque el parmetro haya sido pasado por valor. Por otra parte, las funciones tambin pueden devolver valores de tipo puntero.
void modificar(PPersona& p) ; PPersona buscar(PPersona l, const string& nombre) ;

13.5.

Listas Enlazadas Lineales

Una de las principales aplicaciones de la Memoria Dinmica es el uso de estructuras enlazadas, de tal forma que un campo o atributo de la variable dinmica es a su vez tambin de tipo puntero, por lo que puede apuntar a otra variable dinmica que tambin tenga un campo o atributo de tipo puntero, el cual puede volver a apuntar a otra variable dinmica, y as sucesivamente, tantas veces como sea necesario, hasta que un puntero con el valor NULL indique el nal de la estructura enlazada (lista enlazada). As, en este caso, vemos que un campo de la estructura es de tipo puntero a la propia estructura, por lo que es necesario denir el tipo puntero antes de denir la estructura. Sin embargo, la estructura todava no ha sido denida, por lo que no se puede denir un puntero a ella. Por ello es necesario realizar una declaracin adelantada de un tipo incompleto del tipo de la variable
Dpto. Lenguajes y Ciencias de la Computacin Universidad de Mlaga

13.5. LISTAS ENLAZADAS LINEALES

169

dinmica, donde se declara que un determinado identicador es una estructura o clase, pero no se denen sus componentes.

struct Nodo ; // Declaracin adelantada del tipo incompleto Nodo typedef Nodo* PNodo ; // Definicin de tipo Puntero a tipo incompleto Nodo struct Nodo { // Definicin del tipo Nodo PNodo sig ; // Enlace a la siguiente estructura dinmica string dato ; // Dato almacenado en la lista } ; void escribir(PNodo lista) lista: { pepe juan PNodo ptr = lista; while (ptr != NULL) { cout << ptr->dato << endl ; ptr = ptr->sig ; } } PNodo buscar(PNodo lista, const string& dt) { PNodo ptr = lista ; while ((ptr != NULL)&&(ptr->dato != dt)) { ptr = ptr->sig ; } return ptr ; } PNodo leer_inversa() { PNodo lista = NULL ; string dt ; cin >> dt ; while (dt != "fin") { PNodo ptr = new Nodo ; ptr->dato = dt ; ptr->sig = lista ; lista = ptr ; cin >> dt ; } return lista ; } void destruir(PNodo& lista) { while (lista != NULL) { PNodo ptr = lista ; lista = lista->sig ; delete ptr ; } } int main() { PNodo lista ; lista = leer_inversa() ; escribir(lista) ; PNodo ptr = buscar(lista, "juan"); if (ptr != NULL) { cout << ptr->dato << endl; } destruir(lista) ; }
Dpto. Lenguajes y Ciencias de la Computacin

mara

Universidad de Mlaga

170
Insertar al Principio lista:

CAPTULO 13. MEMORIA DINMICA. PUNTEROS


struct Nodo ; typedef Nodo* PNodo ; struct Nodo { PNodo sig ; string dato ; } ; void insertar_principio(PNodo& lista, const string& dt) { PNodo ptr = new Nodo ; ptr->dato = dt ; ptr->sig = lista ; lista = ptr ; } void insertar_final(PNodo& lista, const string& dt) { PNodo ptr = new Nodo ; ptr->dato = dt ; ptr->sig = NULL ; if (lista == NULL) { lista = ptr ; } else { PNodo act = lista ; while (act->sig != NULL) { act = act->sig ; } act->sig = ptr ; } } PNodo situar(PNodo lista, int pos) { PNodo ptr = lista; while ((ptr != NULL)&&(pos > 0)) { ptr = ptr->sig; --pos; } return ptr; } void insertar_pos(PNodo& lista, int pos, const string& dt) { if (pos < 1) { PNodo ptr = new Nodo ; ptr->dato = dt ; ptr->sig = lista ; lista = ptr ; } else { PNodo ant = situar(lista, pos - 1); if (ant != NULL) { PNodo ptr = new Nodo ; ptr->dato = dt ; ptr->sig = ant->sig ; ant->sig = ptr ; } } }

mara

lista: ptr:

PNodo ptr = new Nodo("pepe") ; mara pepe ptr->sig = lista ;

lista: ptr:

 pepe

3  

mara

lista = ptr ; lista: ptr:

@ R @

3 
 pepe

mara

lista:

pepe

mara

Insertar Detrs ant: pepe

mara

ant:

PNodo ptr = new Nodo("juan") ; pepe mara ptr: juan ptr->sig = ant->sig ;

ant: ptr:

pepe

 juan

3  

mara

ant->sig = ptr ; ant: ptr: Q pepe Q

Q s -

 juan

3  

mara

ant:

pepe

juan

mara

Dpto. Lenguajes y Ciencias de la Computacin

Universidad de Mlaga

13.5. LISTAS ENLAZADAS LINEALES


Eliminar del Principio lista: pepe

171

mara

ptr = lista ; lista: ptr:

@ R @

3 
 pepe

mara

lista = lista->sig ; lista: ptr:  pepe

3  

void eliminar_primero(PNodo& lista) { if (lista != NULL) { PNodo ptr = lista ; lista = lista->sig ; delete ptr ; } } void eliminar_ultimo(PNodo& lista) { if (lista != NULL) { if (lista->sig == NULL) { delete lista ; lista = NULL ; } else { PNodo ant = lista ; PNodo act = ant->sig ; while (act->sig != NULL) { ant = act ; act = act->sig ; } delete act ; ant->sig = NULL ; } } } void eliminar_pos(PNodo& lista, int pos) { if (lista != NULL) { if (pos < 1) { PNodo ptr = lista ; lista = lista->sig ; delete ptr ; } else { PNodo ant = situar(lista, pos - 1) ; if ((ant != NULL)&&(ant->sig != NULL)) { PNodo ptr = ant->sig ; ant->sig = ptr->sig ; delete ptr ; } } } }

mara

delete ptr ; lista: ptr: ?

mara

liberada
lista: mara

Eliminar de Detrs ant: pepe juan

mara

ptr = ant->sig ; ant: ptr: Q pepe Q

Q s -

 juan

3  

mara

ant->sig = ptr->sig ; ant: ptr: pepe

 juan

3  

mara

delete ptr ; ant: ptr: ? pepe

mara

liberada
ant: pepe

mara

Dpto. Lenguajes y Ciencias de la Computacin

Universidad de Mlaga

172

CAPTULO 13. MEMORIA DINMICA. PUNTEROS

void eliminar_elem(PNodo& lista, const string& dt) { void insertar_ord(PNodo& lista, if (lista != NULL) { const string& dt) if (lista->dato == dt) { { PNodo ptr = lista ; PNodo ptr = new Nodo ; lista = lista->sig ; ptr->dato = dt ; delete ptr ; if ((lista==NULL)||(dt < lista->dato)) { } else { ptr->sig = lista ; PNodo ant = lista ; lista = ptr ; PNodo act = ant->sig ; } else { while ((act != NULL)&&(act->dato != dt)) { PNodo ant = lista ; ant = act ; PNodo act = ant->sig ; act = act->sig ; while ((act!=NULL)&&(act->dato<=dt)){ } ant = act ; if (act != NULL) { act = act->sig ; ant->sig = act->sig ; } delete act ; ptr->sig = ant->sig ; } ant->sig = ptr ; } } } } }

PNodo duplicar(PNodo lista) { PNodo nueva = NULL; if (lista != NULL) { nueva = new Nodo ; nueva->dato = lista->dato ; PNodo u = nueva ; PNodo p = lista->sig ; while (p != NULL) { u->sig = new Nodo ; u->sig->dato = p->dato ; u = u->sig ; p = p->sig ; } u->sig = NULL ; } return nueva; }

void purgar(PNodo& lista, const string& dt) { while ((lista != NULL)&&(dt == lista->dato)) { PNodo ptr = lista ; lista = lista->sig ; delete ptr ; } if (lista != NULL) { PNodo ant = lista; PNodo act = lista->sig; while (act != NULL) { if (dt == act->dato) { ant->sig = act->sig ; delete act ; } else { ant = act; } act = ant->sig; } } }

void insertar_ultimo(PNodo& lista, PNodo& ult, const string& dt) { PNodo p = new Nodo ; p->dato = dt ; p->sig = NULL ; if (lista == NULL) { lista = p ; } else { ult->sig = p ; } ult = p ; }
Dpto. Lenguajes y Ciencias de la Computacin

PNodo leer() { PNodo lista = NULL ; PNodo ult = NULL ; string dt ; cin >> dt ; while (dt != "fin") { insertar_ultimo(lista, ult, dt) ; cin >> dt ; } return lista ; }

Universidad de Mlaga

13.6. ABSTRACCIN EN LA GESTIN DE MEMORIA DINMICA

173

13.6.

Abstraccin en la Gestin de Memoria Dinmica

La gestin de memoria dinmica por parte del programador se basa en estructuras de programacin de bajo nivel, las cuales son propensas a errores de programacin y prdida de recursos de memoria. Adems, entremezclar sentencias de gestin de memoria, de bajo nivel, con sentencias aplicadas al dominio de problema a resolver suele dar lugar a cdigo no legible y propenso a errores. Por lo tanto se hace necesario aplicar niveles de abstraccin que aislen la gestin de memoria dinmica (de bajo nivel) del resto del cdigo ms directamente relacionado con la solucin del problema. Para ello, los tipos abstractos de datos proporcionan el mecanismo adecuado para aplicar la abstraccin a estas estructuras de datos basadas en la gestin de memoria dinmica, adems de proporcionar una herramienta adecuada para la gestin de memoria dinmica, ya que los destructores se pueden encargar de liberar los recursos asociados a un determinado objeto. As, vemos que ser necesario duplicar la informacin almacenada en la memoria dinmica al copiar y asignar objetos, as como tambin ser necesario liberar la memoria dinmica antes de asignar nueva informacin o de destruir el objeto. El Destructor debe liberar la memoria dinmica antes de destruir el objeto. El Ctor-Defecto debe inicializar adecuadamente los atributos de tipo puntero. El Ctor-Copia debe duplicar la memoria dinmica del objeto a copiar. El Op-Asignacin debe liberar la memoria dinmica actual y duplicar la memoria dinmica del objeto a asignar. Los mtodos de la clase permiten manipular la estructura de datos, proporcionando abstraccin sobre su complejidad y representacin interna. El acceso restringido a la representacin interna impide una manipulacin externa propensa a errores.
template <typename Tipo> class Lista { public: ~Lista() { destruir(lista) ; } Lista() : sz(0), lista(NULL) { } Lista(const Lista& o) : sz(o.sz), lista(duplicar(o.lista)) { } Lista& operator = (const Lista& o) { if (this != &o) { destruir(lista) ; sz = o.sz ; lista = duplicar(o.lista) ; } return *this ; } void insertar(int pos, const Tipo& d) { ... } void eliminar(int pos) { ... } // ... private: struct Nodo ; typedef Nodo* PNodo ; struct Nodo { PNodo sig ; Tipo dato ; } ; //-- Mtodos privados -void destruir(PNodo& l) const { ... }
Dpto. Lenguajes y Ciencias de la Computacin Universidad de Mlaga

174

CAPTULO 13. MEMORIA DINMICA. PUNTEROS


PNodo duplicar(PNodo l) const { ... } // ... //-- Atributos privados -int sz ; PNodo lista ; } ;

13.7.

Tipo Abstracto de Datos Lista Enlazada Genrica

Aunque las listas enlazadas se pueden programar directamente entremezcladas con el cdigo de resolucin del problema en cuestin, es conveniente que su gestin se realice dentro de una abstraccin que aisle su tratamiento y permita una mejor gestin de sus recursos. Con objeto de facilitar su estudio, el siguiente ejemplo es una implementacin simplicada del tipo abstracto de datos lista genrica de elementos homogneos. Ntese, sin embargo, que otras implementaciones pueden mejorar notablemente su eciencia.
//- lista.hpp ------------------------------------------------------------#ifndef _lista_hpp_ #define _lista_hpp_ #include <cstddef> #include <cassert> namespace umalcc { template <typename Tipo> class Lista { public: //-- Mtodos Pblicos ---------// Destructor ~Lista() { destruir(lista) ; } // Constructor por Defecto Lista() : sz(0), lista(NULL) { } // Constructor de Copia Lista(const Lista& o) : sz(o.sz), lista(duplicar(o.lista)) { } // Operador de Asignacin Lista& operator = (const Lista& o) { if (this != &o) { destruir(lista) ; sz = o.sz ; lista = duplicar(o.lista) ; } return *this ; } // Elimina todos los elementos de la lista actual (queda vacia) void clear() { destruir(lista) ; sz = 0 ; } // Devuelve el numero de elementos almacenados int size() const {
Dpto. Lenguajes y Ciencias de la Computacin Universidad de Mlaga

13.7. TIPO ABSTRACTO DE DATOS LISTA ENLAZADA GENRICA


return sz ; } // Devuelve true si el numero de elementos almacenados // alcanza la capacidad maxima de almacenamiento bool llena() const { return false; } // PRECOND: ( ! llena() && 0 <= pos && pos <= size()) // Inserta (dato) en la lista actual en la posicion (pos) void insertar(int pos, const Tipo& d) { assert(! llena() && 0 <= pos && pos <= size()) ; insertar_pos(lista, pos, d) ; ++sz ; } // PRECOND: (0 <= pos && pos < size()) // Elimina de la lista actual el elemento que ocupa la posicion (pos) void eliminar(int pos) { assert(0 <= pos && pos < size()) ; eliminar_pos(lista, pos) ; --sz ; } // PRECOND: (0 <= pos && pos < size()) // Devuelve el elemento de la lista actual que ocupa la posicion (pos) Tipo acceder(int pos) const { assert(0 <= pos && pos < size()) ; PNodo ptr = situar(lista, pos) ; assert(ptr != NULL) ; return ptr->dato ; }

175

// PRECOND: (0 <= pos && pos < size()) // Asigna (dato) al elemento de la lista actual que ocupa la posicion (pos) void modificar(int pos, const Tipo& d) { assert(0 <= pos && pos < size()) ; PNodo ptr = situar(lista, pos) ; assert(ptr != NULL) ; ptr->dato = d ; } private: //-- Tipos Privados -----struct Nodo ; typedef Nodo* PNodo ; struct Nodo { PNodo sig ; Tipo dato ; } ;

Dpto. Lenguajes y Ciencias de la Computacin

Universidad de Mlaga

176

CAPTULO 13. MEMORIA DINMICA. PUNTEROS


//-- Atributos privados -int sz ; PNodo lista ; //-- Mtodos Privados ---------void destruir(PNodo& lst) const { while (lst != NULL) { PNodo ptr = lst ; lst = lst->sig ; delete ptr ; } } PNodo situar(PNodo lst, int pos) const { PNodo ptr = lst; while ((ptr != NULL)&&(pos > 0)) { ptr = ptr->sig; --pos; } return ptr; } void insertar_pos(PNodo& lst, int pos, const Tipo& dt) const { if (pos < 1) { PNodo ptr = new Nodo ; ptr->dato = dt ; ptr->sig = lst ; lst = ptr ; } else { PNodo ant = situar(lst, pos - 1); if (ant != NULL) { PNodo ptr = new Nodo ; ptr->dato = dt ; ptr->sig = ant->sig ; ant->sig = ptr ; } } } void eliminar_pos(PNodo& lst, int pos) const { if (lst != NULL) { if (pos < 1) { PNodo ptr = lst ; lst = lst->sig ; delete ptr ; } else { PNodo ant = situar(lst, pos - 1) ; if ((ant != NULL)&&(ant->sig != NULL)) { PNodo act = ant->sig ; ant->sig = act->sig ; delete act ; }

Dpto. Lenguajes y Ciencias de la Computacin

Universidad de Mlaga

13.7. TIPO ABSTRACTO DE DATOS LISTA ENLAZADA GENRICA


} } } PNodo duplicar(PNodo lst) const { PNodo nueva = NULL; if (lst != NULL) { nueva = new Nodo ; nueva->dato = lst->dato ; PNodo u = nueva ; PNodo p = lst->sig ; while (p != NULL) { u->sig = new Nodo ; u->sig->dato = p->dato ; u = u->sig ; p = p->sig ; } u->sig = NULL ; } return nueva; } } ; // class } // namespace #endif //-------------------------------------------------------------------------

177

Se puede apreciar como tanto el constructor de copia, como el operador de asignacin duplican la lista almacenada, y por el contrario tanto el destructor como el mtodo clear() liberan todos los recursos que el objeto tenga asignados. As mismo, el mtodo duplicar invoca a la destruccin de los recursos que tuviese antes de duplicar y copiar la nueva lista. A continuacin se puede ver un ejemplo de utilizacin del tipo abstracto de datos lista genrica denido anteriormente.
//- fichero: main.cpp ----------------------------------------------#include <iostream> #include <cctype> #include <cassert> #include "lista.hpp" using namespace std ; using namespace umalcc ; //-----------------------------------------------------------------// Instanciacin de la Lista genrica para almacenar nmeros enteros typedef Lista<int> ListaInt; //-----------------------------------------------------------------void leer_pos(int& pos, int limite) { assert(limite > 0); do { cout << "Introduzca posicion ( < " << limite << " ): " ; cin >> pos; } while (pos < 0 || pos >= limite); } //--------------------------------void leer_dato(int& dato) { cout << "Introduzca un dato: " ; cin >> dato;
Dpto. Lenguajes y Ciencias de la Computacin Universidad de Mlaga

178

CAPTULO 13. MEMORIA DINMICA. PUNTEROS


} //--------------------------------void leer(ListaInt& lista) { int dato ; lista.clear() ; cout << "Introduzca datos (0 -> FIN): " << endl ; cin >> dato ; while ((dato != 0)&&( ! lista.llena())) { lista.insertar(lista.size(), dato) ; cin >> dato ; } } //--------------------------------void escribir(const ListaInt& lista) { cout << "Lista: " ; for (int i = 0 ; i < lista.size() ; ++i) { cout << lista.acceder(i) << " " ; } cout << endl ; } //--------------------------------void prueba_asg(const ListaInt& lista) { cout << "Constructor de Copia" << endl ; ListaInt lst(lista) ; escribir(lst) ; cout << "Operador de Asignacion" << endl ; lst = lista ; escribir(lst) ; } //------------------------------------------------------------------------char menu() { char op ; cout << endl ; cout << "X. Fin" << endl ; cout << "A. Leer Lista" << endl ; cout << "B. Borrar Lista" << endl ; cout << "C. Insertar Posicion" << endl ; cout << "D. Eliminar Posicion" << endl ; cout << "E. Acceder Posicion" << endl ; cout << "F. Modificar Posicion" << endl ; cout << "G. Prueba Copia y Asignacion" << endl ; do { cout << endl << " Opcion: " ; cin >> op ; op = char(toupper(op)) ; } while (!((op == X)||((op >= A)&&(op <= G)))) ; cout << endl ; return op ; } //------------------------------------------------------------------------int main() { ListaInt lista ; int dato ; int pos ;

Dpto. Lenguajes y Ciencias de la Computacin

Universidad de Mlaga

13.7. TIPO ABSTRACTO DE DATOS LISTA ENLAZADA GENRICA


char op = ; do { op = menu() ; switch (op) { case A: leer(lista) ; escribir(lista) ; break ; case B: lista.clear() ; escribir(lista) ; break ; case C: if (lista.llena()) { cout << "Error: Lista llena" << endl ; } else { leer_pos(pos, lista.size()+1) ; leer_dato(dato) ; lista.insertar(pos, dato) ; escribir(lista) ; } break ; case D: if (lista.size() == 0) { cout << "Error: lista vacia" << endl ; } else { leer_pos(pos, lista.size()) ; lista.eliminar(pos) ; escribir(lista) ; } break ; case E: if (lista.size() == 0) { cout << "Error: lista vacia" << endl ; } else { leer_pos(pos, lista.size()) ; cout << "Lista[" << pos << "]: " << lista.acceder(pos) << endl ; escribir(lista) ; } break ; case F: if (lista.size() == 0) { cout << "Error: lista vacia" << endl ; } else { leer_pos(pos, lista.size()) ; leer_dato(dato) ; lista.modificar(pos, dato) ; escribir(lista) ; } break ; case G: prueba_asg(lista) ; break ; } } while (op != X) ; } //- fin: main.cpp ---------------------------------------------------

179

Dpto. Lenguajes y Ciencias de la Computacin

Universidad de Mlaga

180

CAPTULO 13. MEMORIA DINMICA. PUNTEROS

Dpto. Lenguajes y Ciencias de la Computacin

Universidad de Mlaga

Captulo 14

Introduccin a los Contenedores de la Biblioteca Estndar (STL)


Los contenedores de la biblioteca estndar proporcionan un mtodo general para almacenar y acceder a una coleccin de elementos homogneos, proporcionando cada uno de ellos diferentes caractersticas que los hacen adecuados a diferentes necesidades. En este captulo introductorio se mostrarn las principales operaciones que se pueden realizar con los siguientes contenedores: el tipo vector y el tipo deque (para el tipo array vase 6.4). As como con los siguientes adaptadores de contenedores : el tipo stack y el tipo queue de la biblioteca estndar, que implementan el TAD Pila y el TAD Cola respectivamente. La biblioteca estndar tambin dene otros tipos de contenedores optimizados para diferentes circunstancias, pero no sern explicados debido a que su estudio requiere mayores conocimientos que los obtenidos en un curso introductorio.
Contenedor stack (adaptador) queue (adaptador) array vector deque list forward_list map set multimap multiset unordered_map unordered_set unordered_multimap unordered_multiset Tipo TAD Pila TAD Cola Secuencia Secuencia Secuencia Secuencia Secuencia Asociativo Asociativo Asociativo Asociativo Asociativo Asociativo Asociativo Asociativo Acceso Directo (al nal) Directo (al principio) Directo (pos) Directo (pos) Directo (pos) Secuencial (bidir) Secuencial (fw) Binario por clave Binario por clave Binario por clave Binario por clave Hash por clave Hash por clave Hash por clave Hash por clave Insercin Al nal Al nal Al nal Al nal + al principio Cualquier posicin Cualquier posicin Por Clave Por Clave Por Clave Por Clave Por Clave Por Clave Por Clave Por Clave Eliminacin Al nal Al principio Al nal Al nal + Al principio Cualquier posicin Cualquier posicin Por Clave Por Clave Por Clave Por Clave Por Clave Por Clave Por Clave Por Clave

Paso de Parmetros de Contenedores Los contenedores de la biblioteca estndar se pueden pasar como parmetros a subprogramas como cualquier otro tipo compuesto, y por lo tanto se aplican los mecanismos de paso de parmetros para tipos compuestos explicados en la seccin 6.1. Es decir, los parmetros de entrada se pasarn por referencia constante, mientras que los parmetros de salida y entrada/salida se pasarn por referencia. As mismo, como norma general, salvo excepciones, no es adecuado que las funciones retornen valores de tipos de los contenedores, debido a la sobrecarga que generalmente conlleva dicha operacin para el caso de los tipos compuestos. En estos casos suele ser ms adecuado que el valor se devuelva como un parmetro por referencia. 181

182CAPTULO 14. INTRODUCCIN A LOS CONTENEDORES DE LA BIBLIOTECA ESTNDAR (STL)

14.1.

Vector

El contenedor de tipo vector<...> representa una secuencia de elementos homogneos optimizada para el acceso directo a los elementos segn su posicin, as como tambin para la insercin de elementos al nal de la secuencia y para la eliminacin de elementos del nal de la secuencia. Para utilizar un contenedor de tipo vector se debe incluir la biblioteca estndar <vector>, de tal forma que sus deniciones se encuentran dentro del espacio de nombres std:
#include <vector>

El tipo vector es similar al tipo array, salvo en el hecho de que los vectores se caracterizan porque su tamao puede crecer en tiempo de ejecucin dependiendo de las necesidades surgidas durante la ejecucin del programa. Por ello, a diferencia de los arrays, no es necesario especicar un tamao jo y predeterminado en tiempo de compilacin respecto al nmero de elementos que pueda contener. El nmero mximo de elementos que se pueden almacenar en una variable de tipo vector no est especicado, y se pueden almacenar elementos mientras haya capacidad suciente en la memoria del ordenador donde se ejecute el programa. Ntese que en los siguientes ejemplos, por simplicidad, tanto el nmero de elementos como el valor inicial de los mismos estn especicados mediante valores constantes, sin embargo, tambin se pueden especicar como valores de variables y expresiones calculados en tiempo de ejecucin. Instanciacin del Tipo Vector Se pueden denir explcitamente instanciaciones del tipo vector para tipos de elementos concretos mediante la declaracin typedef. Por ejemplo la siguiente denicin declara el tipo Vect_Int como un tipo vector de nmeros enteros.
typedef std::vector<int> Vect_Int ;

Las siguientes deniciones declaran el tipo Matriz como un vector de dos dimensiones de nmeros enteros.
typedef std::vector<int> Fila ; typedef std::vector<Fila> Matriz ;

Construccin de un Objeto de Tipo Vector Se pueden denir variables de un tipo vector previamente denido explcitamente, o directamente de la instanciacin del tipo. Por ejemplo, el siguiente cdigo dene dos variables (v1 y v2) de tipo vector de nmeros enteros, as como la variable m de tipo vector de dos dimensiones de nmeros enteros.
int main() { Vect_Int v1 ; std::vector<int> v2 ; Matriz m ; // ... }

// vector de enteros vaco // vector de enteros vaco // vector de dos dimensiones de enteros vaco

El constructor por defecto del tipo vector crea un objeto vector inicialmente vaco, sin elementos. Posteriormente se podrn aadir y eliminar elementos cuando sea necesario. Tambin es posible crear un objeto vector con un nmero inicial de elementos con un valor inicial por defecto, al que posteriormente se le podrn aadir nuevos elementos. Este nmero inicial de elementos puede ser tanto una constante, como el valor de una variable calculado en tiempo de ejecucin.
Dpto. Lenguajes y Ciencias de la Computacin Universidad de Mlaga

14.1. VECTOR
int main() { Vect_Int v1(10) ; // vector con 10 enteros con valor inicial 0 Matriz m(10, Fila(5)) ; // matriz de 10x5 enteros con valor inicial 0 // ... }

183

As mismo, tambin se puede especicar el valor que tomarn los elementos creados inicialmente.
int main() { Vect_Int v1(10, 3) ; // vector con 10 enteros con valor inicial 3 Matriz m(10, Fila(5, 3)) ; // matriz de 10x5 enteros con valor inicial 3 // ... }

Tambin es posible inicializar un vector con el contenido de otro vector de igual tipo, invocando al constructor de copia:
int main() { Vect_Int Vect_Int Vect_Int Vect_Int // ... }

v1(10, 3) ; // vector con 10 enteros con valor inicial 3 v2(v1) ; // vector con el mismo contenido de v1 v3 = v1 ; // vector con el mismo contenido de v1 v4 = Vect_Int(7, 5) ; // vector con 7 elementos de valor 5

Asignacin de un Objeto de Tipo Vector Es posible la asignacin de vectores de igual tipo. En este caso, se destruye el valor anterior del vector destino de la asignacin.
int main() { Vect_Int v1(10, 3) ; Vect_Int v2 ; v2 = v1 ; v2.assign(5, 7) ; v2 = Vect_Int(5, 7) ; }

// vector con 10 enteros con valor inicial 3 // vector de enteros vaco // asigna el contenido de v1 a v2 // asigna 5 enteros con valor inicial 7 // asigna un vector con 5 elementos de valor 7

As mismo, tambin es posible intercambiar (swap en ingls) de forma eciente el contenido entre dos vectores utilizando el mtodo swap. Por ejemplo:
int main() { Vect_Int v1(10, 5) ; Vect_Int v2(5, 7) ; v1.swap(v2) ; }

// v1 = { 5, 5, 5, 5, 5, 5, 5, 5, 5, 5 } // v2 = { 7, 7, 7, 7, 7 } // v1 = { 7, 7, 7, 7, 7 } // v2 = { 5, 5, 5, 5, 5, 5, 5, 5, 5, 5 }

Control sobre los Elementos de un Vector El nmero de elementos actualmente almacenados en un vector se obtiene mediante el mtodo size(). Por ejemplo:
Dpto. Lenguajes y Ciencias de la Computacin Universidad de Mlaga

184CAPTULO 14. INTRODUCCIN A LOS CONTENEDORES DE LA BIBLIOTECA ESTNDAR (STL)


int main() { Vect_Int v1(10, 3) ; int n = v1.size() ; }

// vector con 10 enteros con valor inicial 3 // nmero de elementos de v1

Es posible tanto aadir un elemento al nal de un vector mediante el mtodo push_back(...), como eliminar el ltimo elemento del vector mediante el mtodo pop_back() (en este caso el vector no debe estar vaco). As mismo, el mtodo clear() elimina todos los elementos del vector. Por ejemplo:
int main() { Vect_Int v(5) ; // v = { 0, for (int i = 1 ; i <= 3 ; ++i) { v.push_back(i) ; } // v = { 0, for (int i = 0 ; i < int(v.size()) ; ++i) { cout << v[i] << " " ; } // muestra: cout << endl ; while (v.size() > 3) { v.pop_back() ; } // v = { 0, v.clear() ; // v = { } }

0, 0, 0, 0 }

0, 0, 0, 0, 1, 2, 3 }

0 0 0 0 0 1 2 3

0, 0 }

Tambin es posible cambiar el tamao del nmero de elementos almacenados en el vector. As, el mtodo resize(...) reajusta el nmero de elementos contenidos en un vector. Si el nmero especicado es menor que el nmero actual de elementos, se eliminarn del nal del vector tantos elementos como sea necesario para reducir el vector hasta el nmero de elementos especicado. Si por el contrario, el nmero especicado es mayor que el nmero actual de elementos, entonces se aadirn al nal del vector tantos elementos como sea necesario para alcanzar el nuevo nmero de elementos especicado (con el valor especicado o con el valor por defecto). Por ejemplo:
int main() { Vect_Int v(10, 1) ; v.resize(5) ; v.resize(9, 2) ; v.resize(7, 3) ; v.resize(10) ; }

// // // // //

v v v v v

= = = = =

{ { { { {

1, 1, 1, 1, 1,

1, 1, 1, 1, 1,

1, 1, 1, 1, 1,

1, 1, 1, 1, 1,

1, 1, 1 } 1, 2, 1, 2, 1, 2,

1, 1, 1, 1 } 2, 2, 2 } 2 } 2, 0, 0, 0 }

Acceso a los Elementos de un Vector Es posible acceder a cada elemento del vector individualmente, segn el ndice de la posicin que ocupe, tanto para obtener su valor almacenado, como para modicarlo mediante el operador de indexacin []. El primer elemento ocupa la posicin cero (0), y el ltimo elemento almacenado en el vector v ocupa la posicin v.size()-1. Por ejemplo:
int main() { Vect_Int for (int v[i] } for (int cout }

v(10) ; i = 0 ; i < int(v.size()) ; ++i) { = i ; i = 0 ; i < int(v.size()) ; ++i) { << v[i] << " " ;

Dpto. Lenguajes y Ciencias de la Computacin

Universidad de Mlaga

14.2. DEQUE
cout << endl ; }

185

El lenguaje de programacin C++ no comprueba que los accesos a los elementos de un vector sean correctos y se encuentren dentro de los lmites vlidos del vector, por lo que ser responsabilidad del programador comprobar que as sea. Sin embargo, en GNU G++, la opcin de compilacin -D_GLIBCXX_DEBUG permite comprobar los ndices de acceso. Tambin es posible acceder a un determinado elemento mediante el mtodo at(i), de tal forma que si el valor del ndice i est fuera del rango vlido, entonces se lanzar una excepcin out_of_range que abortar la ejecucin del programa. Se puede tanto utilizar como modicar el valor de este elemento.
int main() { Vect_Int v(10) ; for (int i = 0 ; i < int(v.size()) ; ++i) { v.at(i) = i ; } for (int i = 0 ; i < int(v.size()) ; ++i) { cout << v.at(i) << " " ; } cout << endl ; }

Comparacin Lexicogrca entre Vectores Es posible realizar la comparacin lexicogrca (==, !=, >, >=, <, <=) entre vectores del mismo tipo siempre y cuando los operadores de comparacin estn denidos para el tipo de los componentes del vector. Por ejemplo:
int main() { Vect_Int v1(10, 7) ; // v1 = { 7, 7, 7, 7, 7, 7, 7, 7, 7, 7 } Vect_Int v2(5, 3) ; // v2 = { 3, 3, 3, 3, 3 } if (v1 == v2) { cout << "Iguales" << endl ; } else { cout << "Distintos" << endl ; } if (v1 < v2) { cout << "Menor" << endl ; } else { cout << "Mayor o Igual" << endl ; } }

14.2.

Deque

El contenedor de tipo deque<...> representa una secuencia de elementos homogneos optimizada para el acceso directo a los elementos segn su posicin, as como tambin para la insercin de elementos al principio y al nal de la secuencia y para la eliminacin de elementos del principio y del nal de la secuencia. Para utilizar un contenedor de tipo deque se debe incluir la biblioteca estndar <deque>, de tal forma que sus deniciones se encuentran dentro del espacio de nombres std:
#include <deque>
Dpto. Lenguajes y Ciencias de la Computacin Universidad de Mlaga

186CAPTULO 14. INTRODUCCIN A LOS CONTENEDORES DE LA BIBLIOTECA ESTNDAR (STL) El contenedor deque presenta el mismo interfaz pblico que el contenedor vector, pero aade dos mtodos nuevos para facilitar la insercin y eliminacin de elementos al principio de la secuencia (push_front(...) y pop_front()). El nmero mximo de elementos que se pueden almacenar en una variable de tipo deque no est especicado, y se pueden almacenar elementos mientras haya capacidad suciente en la memoria del ordenador donde se ejecute el programa. Instanciacin del Tipo Deque Se pueden denir explcitamente instanciaciones del tipo deque para tipos de elementos concretos mediante la declaracin typedef. Por ejemplo la siguiente denicin declara el tipo Deque_Int como un tipo deque de nmeros enteros.
typedef std::deque<int> Deque_Int ;

Construccin de un Objeto de Tipo Deque Se pueden denir variables de un tipo deque previamente denido explcitamente, o directamente de la instanciacin del tipo. Por ejemplo, el siguiente cdigo dene dos variables (v1 y v2) de tipo deque de nmeros enteros.
int main() { Deque_Int v1 ; std::deque<int> v2 ; // ... }

// deque de enteros vaco // deque de enteros vaco

El constructor por defecto del tipo deque crea un objeto deque inicialmente vaco, sin elementos. Posteriormente se podrn aadir y eliminar elementos cuando sea necesario. Tambin es posible crear un objeto deque con un nmero inicial de elementos con un valor inicial por defecto, al que posteriormente se le podrn aadir nuevos elementos. Este nmero inicial de elementos puede ser tanto una constante, como el valor de una variable calculado en tiempo de ejecucin.
int main() { Deque_Int v1(10) ; // ... }

// deque con 10 enteros con valor inicial 0

As mismo, tambin se puede especicar el valor que tomarn los elementos creados inicialmente.
int main() { Deque_Int v1(10, 3) ; // ... }

// deque con 10 enteros con valor inicial 3

Tambin es posible inicializar un deque con el contenido de otro deque de igual tipo, invocando al constructor de copia:
int main() { Deque_Int Deque_Int Deque_Int Deque_Int // ... }

v1(10, 3) ; // deque con 10 v2(v1) ; // deque con el v3 = v1 ; // deque con el v4 = Deque_Int(7, 5) ; // deque

enteros con valor inicial 3 mismo contenido de v1 mismo contenido de v1 con 7 elementos de valor 5

Dpto. Lenguajes y Ciencias de la Computacin

Universidad de Mlaga

14.2. DEQUE Asignacin de un Objeto de Tipo Deque

187

Es posible la asignacin de deques de igual tipo. En este caso, se destruye el valor anterior del deque destino de la asignacin.
int main() { Deque_Int v1(10, 3) ; Deque_Int v2 ; v2 = v1 ; v2.assign(5, 7) ; v2 = Deque_Int(5, 7) ; }

// deque con 10 enteros con valor inicial 3 // deque de enteros vaco // asigna el contenido de v1 a v2 // asigna 5 enteros con valor inicial 7 // asigna un deque con 5 elementos de valor 7

As mismo, tambin es posible intercambiar (swap en ingls) de forma eciente el contenido entre dos deques utilizando el mtodo swap. Por ejemplo:
int main() { Deque_Int v1(10, 5) ; Deque_Int v2(5, 7) ; v1.swap(v2) ; }

// v1 = { 5, 5, 5, 5, 5, 5, 5, 5, 5, 5 } // v2 = { 7, 7, 7, 7, 7 } // v1 = { 7, 7, 7, 7, 7 } // v2 = { 5, 5, 5, 5, 5, 5, 5, 5, 5, 5 }

Control sobre los Elementos de un Deque El nmero de elementos actualmente almacenados en un deque se obtiene mediante el mtodo size(). Por ejemplo:
int main() { Deque_Int v1(10, 3) ; int n = v1.size() ; }

// deque con 10 enteros con valor inicial 3 // nmero de elementos de v1

Es posible tanto aadir un elemento al nal de un deque mediante el mtodo push_back(...), como eliminar el ltimo elemento del deque mediante el mtodo pop_back() (en este caso el deque no debe estar vaco). As mismo, el mtodo clear() elimina todos los elementos del deque. Por ejemplo:
int main() { Deque_Int v(5) ; // v = { 0, 0, 0, 0, 0 } for (int i = 1 ; i <= 3 ; ++i) { v.push_back(i) ; } // v = { 0, 0, 0, 0, 0, 1, 2, 3 } for (int i = 1 ; i <= 2 ; ++i) { v.push_front(i) ; } // v = { 2, 1, 0, 0, 0, 0, 0, 1, 2, 3 } for (int i = 0 ; i < int(v.size()) ; ++i) { cout << v[i] << " " ; } // muestra: 2 1 0 0 0 0 0 1 2 3 cout << endl ; while (v.size() > 5) { v.pop_back() ; } // v = { 2, 1, 0, 0, 0 } while (v.size() > 3) { v.pop_front() ;
Dpto. Lenguajes y Ciencias de la Computacin Universidad de Mlaga

188CAPTULO 14. INTRODUCCIN A LOS CONTENEDORES DE LA BIBLIOTECA ESTNDAR (STL)


} v.clear() ; } // v = { 0, 0, 0 } // v = { }

Tambin es posible cambiar el tamao del nmero de elementos almacenados en el deque. As, el mtodo resize(...) reajusta el nmero de elementos contenidos en un deque. Si el nmero especicado es menor que el nmero actual de elementos, se eliminarn del nal del deque tantos elementos como sea necesario para reducir el deque hasta el nmero de elementos especicado. Si por el contrario, el nmero especicado es mayor que el nmero actual de elementos, entonces se aadirn al nal del deque tantos elementos como sea necesario para alcanzar el nuevo nmero de elementos especicado (con el valor especicado o con el valor por defecto). Por ejemplo:
int main() { Deque_Int v(10, 1) ; v.resize(5) ; v.resize(9, 2) ; v.resize(7, 3) ; v.resize(10) ; }

// // // // //

v v v v v

= = = = =

{ { { { {

1, 1, 1, 1, 1,

1, 1, 1, 1, 1,

1, 1, 1, 1, 1,

1, 1, 1, 1, 1,

1, 1, 1 } 1, 2, 1, 2, 1, 2,

1, 1, 1, 1 } 2, 2, 2 } 2 } 2, 0, 0, 0 }

Acceso a los Elementos de un Deque Es posible acceder a cada elemento del deque individualmente, segn el ndice de la posicin que ocupe, tanto para obtener su valor almacenado, como para modicarlo mediante el operador de indexacin []. El primer elemento ocupa la posicin cero (0), y el ltimo elemento almacenado en el deque v ocupa la posicin v.size()-1. Por ejemplo:
int main() { Deque_Int v(10) ; for (int i = 0 ; i < int(v.size()) ; ++i) { v[i] = i ; } for (int i = 0 ; i < int(v.size()) ; ++i) { cout << v[i] << " " ; } cout << endl ; }

El lenguaje de programacin C++ no comprueba que los accesos a los elementos de un deque sean correctos y se encuentren dentro de los lmites vlidos del deque, por lo que ser responsabilidad del programador comprobar que as sea. Sin embargo, en GNU G++, la opcin de compilacin -D_GLIBCXX_DEBUG permite comprobar los ndices de acceso. Tambin es posible acceder a un determinado elemento mediante el mtodo at(i), de tal forma que si el valor del ndice i est fuera del rango vlido, entonces se lanzar una excepcin out_of_range que abortar la ejecucin del programa. Se puede tanto utilizar como modicar el valor de este elemento.
int main() { Deque_Int v(10) ; for (int i = 0 ; i < int(v.size()) ; ++i) { v.at(i) = i ; } for (int i = 0 ; i < int(v.size()) ; ++i) { cout << v.at(i) << " " ; } cout << endl ; }
Dpto. Lenguajes y Ciencias de la Computacin Universidad de Mlaga

14.3. STACK Comparacin Lexicogrca entre Deques

189

Es posible realizar la comparacin lexicogrca (==, !=, >, >=, <, <=) entre deques del mismo tipo siempre y cuando los operadores de comparacin estn denidos para el tipo de los componentes del deque. Por ejemplo:
int main() { Deque_Int v1(10, 7) ; // v1 = { 7, 7, 7, 7, 7, 7, 7, 7, 7, 7 } Deque_Int v2(5, 3) ; // v2 = { 3, 3, 3, 3, 3 } if (v1 == v2) { cout << "Iguales" << endl ; } else { cout << "Distintos" << endl ; } if (v1 < v2) { cout << "Menor" << endl ; } else { cout << "Mayor o Igual" << endl ; } }

14.3.

Stack

El adaptador de contenedor de tipo stack<...> representa el tipo abstracto de datos Pila, como una coleccin ordenada (segn el orden de insercin) de elementos homogneos donde se pueden introducir elementos (manteniendo el orden de insercin) y sacar elementos de ella (en orden inverso al orden de insercin), de tal forma que el primer elemento que sale de la pila es el ltimo elemento que ha sido introducido en ella. Adems, tambin es posible comprobar si la pila contiene elementos, de tal forma que no se podr sacar ningn elemento de una pila vaca. Para utilizar un adaptador de contenedor de tipo stack se debe incluir la biblioteca estndar <stack>, de tal forma que sus deniciones se encuentran dentro del espacio de nombres std:
#include <stack>

El nmero mximo de elementos que se pueden almacenar en una variable de tipo stack no est especicado, y se pueden introducir elementos mientras haya capacidad suciente en la memoria del ordenador donde se ejecute el programa. Instanciacin del Tipo Stack Se pueden denir explcitamente instanciaciones del tipo stack para tipos de elementos concretos mediante la declaracin typedef. Por ejemplo la siguiente denicin declara el tipo Stack_Int como un tipo pila de nmeros enteros.
typedef std::stack<int> Stack_Int ;

Construccin de un Objeto de Tipo Pila Se pueden denir variables de un tipo pila previamente denido explcitamente, o directamente de la instanciacin del tipo. Por ejemplo, el siguiente cdigo dene dos variables (s1 y s2) de tipo pila de nmeros enteros.
int main() { Stack_Int s1 ; std::stack<int> s2 ; // ... }

// stack de enteros vaco // stack de enteros vaco

Dpto. Lenguajes y Ciencias de la Computacin

Universidad de Mlaga

190CAPTULO 14. INTRODUCCIN A LOS CONTENEDORES DE LA BIBLIOTECA ESTNDAR (STL) El constructor por defecto del tipo stack crea un objeto stack inicialmente vaco, sin elementos. Posteriormente se podrn aadir y eliminar elementos cuando sea necesario. Tambin es posible inicializar una pila con el contenido de otra pila de igual tipo:
int main() { Stack_Int // ... Stack_Int Stack_Int Stack_Int // ... }

s1 ;

// stack de enteros vaco

s2(s1) ; // stack con el mismo contenido de s1 s3 = s1 ; // stack con el mismo contenido de s1 s4 = Stack_Int() ; // copia el contenido de stack vaco

Asignacin de un Objeto de Tipo Pila Es posible la asignacin de pilas de igual tipo. En este caso, se destruye el valor anterior de la pila destino de la asignacin.
int main() { Stack_Int s1 ; Stack_Int s2 ; s2 = s1 ; s2 = Stack_Int() ; }

// stack de enteros vaco // stack de enteros vaco // asigna el contenido de s1 a s2 // asigna el contenido de stack vaco

Control y Acceso a los Elementos de una Pila Es posible tanto aadir un elemento una pila mediante el mtodo push(...), como eliminar el ltimo elemento introducido en la pila mediante el mtodo pop() (en este caso la pila no debe estar vaca). Por otra parte, el mtodo empty() indica si una pila est vaca o no, mientras que el nmero de elementos actualmente almacenados en una pila se obtiene mediante el mtodo size(). As mismo, se puede acceder al ltimo elemento introducido en la pila mediante el mtodo top(). Se puede tanto utilizar como modicar el valor de este elemento (en este caso la pila no debe estar vaca). Por ejemplo:
int main() { Stack_Int s ; for (int i = 1 ; i <= 3 ; ++i) { s.push(i) ; } s.top() = 5 ; s.pop() ; s.pop() ; s.push(7) ; s.push(9) ; cout << s.size() << endl ; while (! s.empty()) { cout << s.top() << " " ; s.pop() ;
Dpto. Lenguajes y Ciencias de la Computacin

// s = { }

// s = { 1, 2, 3 } // s = { 1, 2, 5 } // // // // s s s s = = = = { { { { 1, 2 } 1 } 1, 7 } 1, 7, 9 }

// muestra: 3

// muestra: 9 7 1

Universidad de Mlaga

14.4. QUEUE
} cout << endl ; } // s = { }

191

Comparacin Lexicogrca entre Pilas Es posible realizar la comparacin lexicogrca (==, !=, >, >=, <, <=) entre pilas del mismo tipo siempre y cuando los operadores de comparacin estn denidos para el tipo de los componentes de la pila. Por ejemplo:
int main() { Stack_Int s1 ; Stack_Int s2 ; // ... if (s1 == s2) { cout << "Iguales" << endl ; } else { cout << "Distintos" << endl ; } if (s1 < s2) { cout << "Menor" << endl ; } else { cout << "Mayor o Igual" << endl ; } }

14.4.

Queue

El adaptador de contenedor de tipo queue<...> representa el tipo abstracto de datos Cola, como una coleccin ordenada (segn el orden de insercin) de elementos homogneos donde se pueden introducir elementos (manteniendo el orden de insercin) y sacar elementos de ella (en el mismo orden al orden de insercin), de tal forma que el primer elemento que sale de la cola es el primer elemento que ha sido introducido en ella. Adems, tambin es posible comprobar si la cola contiene elementos, de tal forma que no se podr sacar ningn elemento de una cola vaca. Para utilizar un adaptador de contenedor de tipo queue se debe incluir la biblioteca estndar <queue>, de tal forma que sus deniciones se encuentran dentro del espacio de nombres std:
#include <queue>

El nmero mximo de elementos que se pueden almacenar en una variable de tipo queue no est especicado, y se pueden introducir elementos mientras haya capacidad suciente en la memoria del ordenador donde se ejecute el programa. Instanciacin del Tipo Queue Se pueden denir explcitamente instanciaciones del tipo queue para tipos de elementos concretos mediante la declaracin typedef. Por ejemplo la siguiente denicin declara el tipo Queue_Int como un tipo cola de nmeros enteros.
typedef std::queue<int> Queue_Int ;

Construccin de un Objeto de Tipo Cola Se pueden denir variables de un tipo cola previamente denido explcitamente, o directamente de la instanciacin del tipo. Por ejemplo, el siguiente cdigo dene dos variables (c1 y c2) de tipo cola de nmeros enteros.
Dpto. Lenguajes y Ciencias de la Computacin Universidad de Mlaga

192CAPTULO 14. INTRODUCCIN A LOS CONTENEDORES DE LA BIBLIOTECA ESTNDAR (STL)


int main() { Queue_Int c1 ; std::queue<int> c2 ; // ... }

// queue de enteros vaco // queue de enteros vaco

El constructor por defecto del tipo queue crea un objeto queue inicialmente vaco, sin elementos. Posteriormente se podrn aadir y eliminar elementos cuando sea necesario. Tambin es posible inicializar una cola con el contenido de otra cola de igual tipo:
int main() { Queue_Int // ... Queue_Int Queue_Int Queue_Int // ... }

c1 ;

// queue de enteros vaco

c2(c1) ; // queue con el mismo contenido de c1 c3 = c1 ; // queue con el mismo contenido de c1 c4 = Stack_Int() ; // copia el contenido de queue vaco

Asignacin de un Objeto de Tipo Cola Es posible la asignacin de colas de igual tipo. En este caso, se destruye el valor anterior de la cola destino de la asignacin.
int main() { Queue_Int c1 ; Queue_Int c2 ; c2 = c1 ; c2 = Queue_Int() ; }

// queue de enteros vaco // queue de enteros vaco // asigna el contenido de c1 a c2 // asigna el contenido de queue vaco

Control y Acceso a los Elementos de una Cola Es posible tanto aadir un elemento una cola mediante el mtodo push(...), como eliminar el primer elemento introducido en la cola mediante el mtodo pop() (en este caso la cola no debe estar vaca). Por otra parte, el mtodo empty() indica si una cola est vaca o no, mientras que el nmero de elementos actualmente almacenados en una cola se obtiene mediante el mtodo size(). As mismo, se puede acceder al ltimo elemento introducido en la cola mediante el mtodo back(), as como al primer elemento introducido en ella mediante el mtodo front(). Se pueden tanto utilizar como modicar el valor de estos elementos (en este caso la cola no debe estar vaca). Por ejemplo:
int main() { Queue_Int c ; for (int i = 1 ; i <= 3 ; ++i) { c.push(i) ; } c.front() = 6 ; c.back() = 5 ; c.pop() ; c.pop() ;
Dpto. Lenguajes y Ciencias de la Computacin

// c = { }

// c = { 1, 2, 3 } // c = { 6, 2, 3 } // c = { 6, 2, 5 } // c = { 2, 5 } // c = { 5 }
Universidad de Mlaga

14.5. RESOLUCIN DE PROBLEMAS UTILIZANDO CONTENEDORES


c.push(7) ; c.push(9) ; cout << c.size() << endl ; while (! c.empty()) { cout << c.front() << " " ; c.pop() ; } cout << endl ; } // c = { 5, 7 } // c = { 5, 7, 9 } // muestra: 3

193

// muestra: 5 7 9 // c = { }

Comparacin Lexicogrca entre Colas Es posible realizar la comparacin lexicogrca (==, !=, >, >=, <, <=) entre colas del mismo tipo siempre y cuando los operadores de comparacin estn denidos para el tipo de los componentes de la cola. Por ejemplo:
int main() { Queue_Int c1 ; Queue_Int c2 ; // ... if (c1 == c2) { cout << "Iguales" << endl ; } else { cout << "Distintos" << endl ; } if (c1 < c2) { cout << "Menor" << endl ; } else { cout << "Mayor o Igual" << endl ; } }

14.5.

Resolucin de Problemas Utilizando Contenedores

Ejemplo 1: Agentes de Ventas Disee un programa que lea y almacene las ventas realizadas por unos agentes de ventas, de tal forma que se eliminen aquellos agentes cuyas ventas sean inferiores a la media de las ventas realizadas.
//-----------------------------------------------------------------#include <iostream> #include <vector> #include <string> using namespace std ; struct Agente { string nombre ; double ventas ; } ; typedef vector<Agente> VAgentes ; void leer (VAgentes& v) { v.clear() ;
Dpto. Lenguajes y Ciencias de la Computacin Universidad de Mlaga

194CAPTULO 14. INTRODUCCIN A LOS CONTENEDORES DE LA BIBLIOTECA ESTNDAR (STL)


Agente a ; cout << "Introduzca Nombre: " ; getline(cin, a.nombre) ; while (( ! cin.fail()) && (a.nombre.size() > 0)) { cout << "Introduzca Ventas: " ; cin >> a.ventas ; cin.ignore(1000, \n) ; v.push_back(a) ; cout << "Introduzca Nombre: " ; getline(cin, a.nombre) ; } } double media(const VAgentes& v) { double suma=0.0 ; for (int i = 0 ; i < int(v.size()) ; ++i) { suma += v[i].ventas ; } return suma/double(v.size()) ; } void purgar(VAgentes& v, double media) { // altera el orden secuencial de los elementos int i = 0 ; while (i < int(v.size())) { if (v[i].ventas < media) { v[i] = v[v.size()-1] ; v.pop_back() ; } else { ++i ; } } } void purgar_ordenado(VAgentes& v, double media) { // mantiene el orden secuencial de los elementos int k = 0 ; while ((k < int(v.size()))&&(v[k].ventas >= media)) { ++k; } for (int i = k ; i < int(v.size()) ; ++i) { if(v[i].ventas >= media) { v[k] = v[i] ; ++k ; } } v.resize(k) ; } void imprimir(const VAgentes& v) { for (int i = 0 ; i < int(v.size()) ; ++i) { cout << v[i].nombre << " " << v[i].ventas << endl ; } }

Dpto. Lenguajes y Ciencias de la Computacin

Universidad de Mlaga

14.5. RESOLUCIN DE PROBLEMAS UTILIZANDO CONTENEDORES


int main () { VAgentes v ; leer(v) ; purgar(v, media(v)) ; imprimir(v) ; } //------------------------------------------------------------------

195

Ejemplo 2: Multiplicacin de Matrices Disee un programa que lea dos matrices de tamaos arbitrarios y muestre el resultado de multiplicar ambas matrices.
//-----------------------------------------------------------------#include <vector> #include <iostream> #include <iomanip> using namespace std ; typedef vector <double> Fila ; typedef vector <Fila> Matriz ; void imprimir(const Matriz& m) { for (int f = 0 ; f < int(m.size()) ; ++f) { for (int c = 0 ; c < int(m[f].size()) ; ++c) { cout << setw(10) << setprecision(4) << m[f][c] << " " ; } cout << endl ; } } void leer(Matriz& m) { int nf, nc ; cout << "Introduzca el numero de filas: " ; cin >> nf ; cout << "Introduzca el numero de columnas: " ; cin >> nc ; m = Matriz(nf, Fila (nc)) ; // copia de la matriz completa cout << "Introduzca los elementos: " << endl ; for (int f = 0 ; f < int(m.size()) ; ++f) { for (int c = 0 ; c < int(m[f].size()) ; ++c) { cin >> m[f][c] ; } } } // otra opcin ms eficiente para la lectura de vectores void leer_2(Matriz& m) { int nf, nc ; cout << "Introduzca el numero de filas: " ; cin >> nf ; cout << "Introduzca el numero de columnas: " ; cin >> nc ; Matriz aux(nf, Fila (nc)) ; cout << "Introduzca los elementos: " << endl ;
Dpto. Lenguajes y Ciencias de la Computacin Universidad de Mlaga

196CAPTULO 14. INTRODUCCIN A LOS CONTENEDORES DE LA BIBLIOTECA ESTNDAR (STL)


for (int f = 0 for (int c cin >> } } m.swap(aux) ; } void multiplicar(const Matriz& m1, const Matriz& m2, Matriz& m3) { m3.clear() ; if ((m1.size() > 0) && (m2.size() > 0) && (m2[0].size() > 0) && (m1[0].size() == m2.size())){ Matriz aux(m1.size(), Fila(m2[0].size())) ; for (int f = 0 ; f < int(aux.size()) ; ++f) { for (int c = 0 ; c < int(aux[f].size()) ; ++c) { double suma = 0.0 ; for (int k = 0 ; k < int(m2.size()) ; ++k) { suma += m1[f][k] * m2[k][c] ; } aux[f][c] = suma ; } } m3.swap(aux) ; // evita la copia de la matriz completa } } int main() { Matriz m1, m2, m3 ; leer(m1) ; leer(m2) ; multiplicar(m1, m2, m3) ; if (m3.size() == 0) { cout << "Error en la multiplicacin de Matrices" << endl ; } else { imprimir(m3) ; } } //-----------------------------------------------------------------; f < int(aux.size()) ; ++f) { = 0 ; c < int(aux[f].size()) ; ++c) { aux[f][c] ;

// evita la copia de la matriz completa

Dpto. Lenguajes y Ciencias de la Computacin

Universidad de Mlaga

Captulo 15

Introduccin a la Programacin Orientada a Objetos


La Programacin Orientada a Objetos extiende los conceptos fundamentales de abstraccin y encapsulacin de los Tipos Abstractos de Datos (TADs), aadiendo los conceptos de Herencia, Polimorsmo, y Vinculacin Dinmica.

Clase
op1() op2() op3()

Se mantiene el concepto de Objetos como instancias de Clases, los cuales son entidades activas que encapsulan datos y algoritmos, donde los Atributos contienen el estado y la representacin interna del objeto, cuyo acceso est restringido, y los Mtodos permiten la manipulacin e interaccin entre objetos La Programacin Orientada a Objetos proporciona un mecanismo adecuado para el diseo y desarrollo de software complejo, modular, reusable, adaptable y extensible.

15.1.

Herencia

La Herencia permite modelar relaciones ES UN entre clases, y denir jerarquas de clases. Permite denir una nueva clase derivada (o sub-clase) como una especializacin o extensin de una clase base (o super-clase) ms general, donde la clase derivada hereda tanto los atributos, como los mtodos denidos por la clase base (reusabilidad del cdigo), y la clase derivada puede denir nuevos atributos y nuevos mtodos (extensibilidad), as como redenir los mtodos de la clase base.
Animal Ventana

Mamifero

Ave

Interactiva

Texto

Humano

Chimpanc

Menu

Botn

197

198 CAPTULO 15. INTRODUCCIN A LA PROGRAMACIN ORIENTADA A OBJETOS En C++, se puede denir una clase derivada de una clase base especicando el delimitador (:) despus del identicador de la clase derivada, seguida de la palabra reservada public (para especicar herencia pblica) y del identicador de la clase base de la que hereda. Las relaciones de herencia pueden mostrarse adecuadamente utilizando los diagramas UML de clases. As mismo, tambin es posible denir un nuevo mbito de visibilidad (protected) como un mbito de acceso restringido que permite el acceso desde la propia clase as como tambin desde las clases derivadas. Los constructores de las clases derivadas pueden invocar explcitamente, en la lista de inicializacin, a los contructores de las clases bases. Ntese que en el siguiente ejemplo se hace uso de la palabra reservada virtual, cuyo signicado ser explicado en una seccin posterior (vase 15.3).
class Vehiculo { public: virtual ~Vehiculo() {} Vehiculo() : pos() {} virtual void mover() { ++pos; } virtual void estacionar() { pos = 0; } protected: virtual void mover(int x) { pos += x; } private: int pos; }; class Bicicleta : public Vehiculo { public: Bicicleta() : Vehiculo(), plato() {} virtual void cambiar() { plato = (plato + 1) % 3 ; } private: int plato; }; class Automovil : public Vehiculo { public: Automovil() : Vehiculo(), depos() {} virtual void repostar(int lit) { depos += lit ; } private: int depos; }; int main() { Vehiculo* v = new Vehiculo ; Automovil* a = new Automovil ; Bicicleta* b = new Bicicleta ; v->mover() ; a->estacionar() ; b->mover() ; a->repostar(5) ; b->cambiar() ; delete v ; delete a ; delete b ; }
Dpto. Lenguajes y Ciencias de la Computacin Universidad de Mlaga

Vehiculo
posicin mover(...) estacionar(...)

Automovil
depsito repostar(...)

Bicicleta
plato cambiar(...)

15.2. POLIMORFISMO

199

15.2.

Polimorsmo

El polimorsmo permite que un puntero (o referencia) a un objeto de una clase derivada pueda ser considerado como un puntero a un objeto de la clase base, por lo que un puntero a un objeto derivado es tambin a su vez un puntero a un objeto de la clase base. No obstante, la direccin de correspondencia opuesta no se mantiene, ya que no todos los punteros a objetos de la clase base son tambin punteros a objetos de la clase derivada. Para realizar la conversin opuesta, se debe utilizar el siguiente operador de conversin dinmica: dynamic_cast<Derivada*>(ptr_base), el cual produce NULL si no es adecuada la conversin en tiempo de ejecucin. Para que el polimorsmo sea efectivo, se debe trabajar con los objetos a travs de punteros o de referencias. As mismo, hay que tener en cuenta que el operador de asignacin no funciona adecuadamente en estos contextos polimrcos, por lo que usualmente se suele sustituir por un mtodo clone().
class Base { public: virtual ~Base() {} Base() {} virtual void metodos(...) { } virtual Base* clone() const { } private: Base& operator=(const Base&); }; class Derivada: public Base { public: virtual ~Derivada() {} Derivada() {} virtual void metodos(...) { } virtual Derivada* clone() const { } private: // atributos ... }; void proc(Base* b) { b->metodos(); } int main() { Base* b = new Derivada ; b->metodos(); Derivada* d = dynamic_cast<Derivada*>(b); proc(d); delete b; }

15.3.

Vinculacin Dinmica

La vinculacin dinmica permite que las clases derivadas puedan redenir el comportamiento de los mtodos denidos en la clase base. As, en contextos polimrcos, los mtodos invocados se seleccionan adecuadamente, en tiempo de ejecucin, dependiendo del tipo real, y no del tipo aparente, del objeto sobre el que se aplica la invocacin del mtodo. Para poder utilizar la vinculacin dinmica en C++, se debe anteponer la palabra reservada virtual a todos los mtodos de la clase, incluyendo explcitamente al destructor, excepto a los constructores. Adems, para que el polimorsmo y la vinculacin dinmica sean efectivos, se debe trabajar con los objetos a travs de punteros o de referencias.
Dpto. Lenguajes y Ciencias de la Computacin Universidad de Mlaga

200 CAPTULO 15. INTRODUCCIN A LA PROGRAMACIN ORIENTADA A OBJETOS


class Vehiculo { public: virtual ~Vehiculo() {} Vehiculo() : pos() {} virtual void mover() { ++pos; } virtual void estacionar() { pos = 0; } virtual Vehiculo* clone() const { return new Vehiculo(*this); } protected: virtual void mover(int x) { pos += x; } private: Vehiculo& operator=(const Vehiculo&); int pos; };

Vehiculo class Automovil : public Vehiculo { public: posicin Automovil() : Vehiculo(), depos() {} mover(...) virtual void repostar(int lit) estacionar(...) { depos += lit ; } clone(...) virtual void mover() { if (depos > 0) { Vehiculo::mover(20); // Invocacin C.Base --depos; } Automovil Bicicleta } plato depsito virtual Automovil* clone() const { return new Automovil(*this); } cambiar(...) repostar(...) private: mover(...) mover(...) clone(...) clone(...) int depos; };
class Bicicleta : public Vehiculo { public: Bicicleta() : Vehiculo(), plato() {} virtual void cambiar() { plato = (plato + 1) % 3 ; } virtual void mover() { Vehiculo::mover(); // Invocacin C.Base Vehiculo::mover(plato); // Invocacin C.Base } virtual Bicicleta* clone() const { return new Bicicleta(*this); } private: int plato; }; int main() { Vehiculo* a = new Automovil ; Vehiculo* b = new Bicicleta ; a->mover() ; b->mover() ; Vehiculo* v = a->clone(); v->mover() ; delete v ; delete a ; delete b ; }
Dpto. Lenguajes y Ciencias de la Computacin Universidad de Mlaga

15.4. CLASES BASE ABSTRACTAS. INTERFACES

201

15.4.

Clases Base Abstractas. Interfaces

Una clase base puede especicar algunos (o todos) los mtodos como abstractos (utilizando el smbolo =0), de tal forma que no necesita denirlos. En este caso se denomina una clase base abstracta (ABC Abstract Base Class ). Las clases derivadas podrn implementar los mtodos abstractos adecuadamente. En caso de que una clase derivada no proporcione la implementacin de todos los mtodos abstractos, entonces tambin ser una clase abstracta. As, no es posible instanciar un objeto de una clase abstracta. El objeto se debe instanciar con alguna clase derivada que no sea abstracta. Cuando una clase base abstracta no dene ningn atributo y todos sus mtodos son abstractos, entonces se dice que dene una clase base abstracta pura o interfaz. Un interfaz dene una abstraccin, un comportamiento abstracto, y las clases derivadas debern implementar dicho comportamiento. Las clases abstractas y los mtodos abstractos se representan con un tipo de letra inclinada en los diagramas UML de clases.
class Vehiculo { public: virtual ~Vehiculo() {} Vehiculo() : pos() {} virtual void mover() =0; virtual void estacionar() virtual Vehiculo* clone() protected: virtual void mover(int x) private: Vehiculo& operator=(const int pos; };

{ pos = 0; } const =0; { pos += x; } Vehiculo&);

Vehiculo
posicin mover(...) estacionar(...) clone(...)

o incluso como interfaz:


class Vehiculo { public: virtual void mover() =0; virtual void estacionar() =0; virtual Vehiculo* clone() const =0; private: Vehiculo& operator=(const Vehiculo&); };

Vehiculo
mover(...) estacionar(...) clone(...)

Dpto. Lenguajes y Ciencias de la Computacin

Universidad de Mlaga

202 CAPTULO 15. INTRODUCCIN A LA PROGRAMACIN ORIENTADA A OBJETOS

15.5.

Herencia Mltiple Virtual

Una clase puede ser derivada de mltiples clases base (herencia mltiple). Sin embargo, puede dar lugar a situaciones complejas. La herencia mltiple puede ser gestionada fcilmente si una clase derivada slo hereda directamente de una nica clase base, y las dems herencias se realizan de forma virtual de interfaces. As mismo, las interfaces slo deben heredar de forma virtual de otras interfaces.
class Cloneable { public: virtual Cloneable* clone() const =0; private: Cloneable& operator=(const Cloneable&); }; class Vehiculo { public: virtual ~Vehiculo() {} Vehiculo() : pos() {} virtual void mover() =0; virtual void estacionar() { pos = 0; } protected: virtual void mover(int x) { pos += x; } private: int pos; }; class Bicicleta : public Vehiculo, public virtual Cloneable { public: Bicicleta() : Vehiculo(), plato() {} virtual void cambiar() { plato = (plato + 1) % 3 ; } virtual void mover() { Vehiculo::mover(plato); } virtual Bicicleta* clone() const { return new Bicicleta(*this); } private: int plato; };

Vehiculo
posicion mover(...) estacionar(...)

Cloneable
clone(...)

Bicicleta
plato cambiar(...) mover(...) clone(...)

Dpto. Lenguajes y Ciencias de la Computacin

Universidad de Mlaga

Captulo 16

Bibliografa
El Lenguaje de Programacin C. 2.Ed. B.Kernighan, D. Ritchie Prentice Hall 1991 The C++ Programming Language. Special Edition B. Stroustrup Addison Wesley 2000

203

ndice alfabtico
mbito de visibilidad, 32 agregado, 67 predenido multidimensional, 74 array, 67 predenido multidimensional, 74 bsqueda binaria, 87 lineal, 86 2d, 87 biblioteca ansic cctype, 97 cmath, 97 cstdlib, 98 bloque, 31 buer, 25 de entrada, 25 de salida, 25 cin, 27 comentarios, 13 compilacin separada, 119 constantes literales, 17 simblicas, 17 declaracin, 18 conversiones de tipo automticas, 21 explcitas, 22 cout, 26 declaracin global, 31 mbito de visibilidad, 31 local, 32 mbito de visibilidad, 32 declaracion adelantada, 168 denicin vs. declaracin, 15 delete, 165 delimitadores, 13 ejecucin secuencial, 31 enlazado, 119 entrada, 27 espacios de nombre annimos, 123 espacios de nombres, 120, 121 using namespace, 122 espacios en blanco, 13 estructura, 63 chero de encabezamiento guardas, 118 funciones, 46 declaracin, 51 inline, 51 return, 47 guardas, 118 inline, 51 listas enlazadas declaracion adelantada, 168 mdulo implementacin, 117 interfaz, 117 main, 11 memoria dinmica, 165 abstraccion, 173 delete, 165 enlaces, 168 listas enlazadas genrica, 174 new, 165 new, 165 operadores, 13 aritmticos, 19 bits, 19 condicional, 20 lgicos, 20 relacionales, 19 ordenacin burbuja, 89 204

NDICE ALFABTICO insercin, 90 intercambio, 89 seleccin, 88 parmetros de entrada, 48 parmetros de entrada/salida, 49 parmetros de salida, 48 paso por referencia constante, 55 paso por referencia, 49 paso por valor, 48 procedimientos, 45 declaracin, 51 inline, 51 prototipo, 51 registro, 63 return, 47 salida, 26 secuencia de sentencias, 31 sentencia asignacin, 20 incremento/decremento, 20 iteracin, 37 do while, 40 for, 38 while, 37 seleccin, 32 if, 33 switch, 35 tipo, 15 tipos cuadro resumen, 16 puntero, 164 acceso, 166 operaciones, 166 parmetros, 168 tipos simples predenidos short, 16 unsigned, 16 tipos compuestos array, 67 tipos simples escalares, 16 tipos compuestos array, 67 struct, 63 tipos simples predenidos, 16 tipos compuestos, 15, 55 parmetros, 55 tipos simples, 15 enumerado, 17
Dpto. Lenguajes y Ciencias de la Computacin

205 ordinales, 16 predenidos bool, 16 char, 16 double, 16 oat, 16 int, 16 long, 16 long long, 16 using namespace, 122 variables declaracin, 18

Universidad de Mlaga

You might also like