Programacion C++
Programacion C++
Fundamentos de Programación
con
el Lenguaje de Programación
C++
23 de octubre de 2017
2
Prólogo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
I Programación Básica 9
1. Un Programa C++ 11
2. Tipos Simples 15
2.1. Declaración Vs. Definición . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
2.2. Tipos Simples Predefinidos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
2.3. Tipos Simples Enumerados . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
2.4. Constantes y Variables . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
2.5. Expresiones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
2.6. Sentencias de Asignación . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
2.7. Visibilidad de los identificadores . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
2.8. Conversiones Automáticas (Implícitas) de Tipos . . . . . . . . . . . . . . . . . . . . 22
2.9. Conversiones Explícitas de Tipos . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
2.10. Tabla ASCII . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
2.11. Algunas consideraciones respecto a operaciones con números reales . . . . . . . . . 23
4. Estructuras de Control 31
4.1. Sentencia, Secuencia y Bloque . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
4.2. Declaraciones Globales y Locales . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
4.3. Sentencias de Selección . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
4.4. Sentencias de Iteración. Bucles . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
4.5. Programación Estructurada . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41
4.6. Ejemplos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42
3
4 CONTENIDO
6. Tipos Compuestos 55
6.1. Paso de Parámetros de Tipos Compuestos . . . . . . . . . . . . . . . . . . . . . . . 55
6.2. Cadenas de Caracteres en C++: el Tipo string . . . . . . . . . . . . . . . . . . . 56
6.2.1. Entrada y Salida de Cadenas de Caracteres . . . . . . . . . . . . . . . . . . 56
6.2.2. Operaciones predefinidas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59
6.2.3. Ejemplos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61
6.3. Registros o Estructuras . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 64
6.3.1. Operaciones con registros completos . . . . . . . . . . . . . . . . . . . . . . 65
6.3.2. Entrada/Salida de valores de tipo registro . . . . . . . . . . . . . . . . . . . 66
6.3.3. Ejemplo. Uso de registros . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66
6.4. Agregados: el Tipo Array . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68
6.4.1. Operaciones predefinidas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 70
6.4.2. Ejemplos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71
6.4.3. Listas o Secuencias con Número Variable de Elementos. Agregados Incompletos 73
6.4.4. Agregados Multidimensionales . . . . . . . . . . . . . . . . . . . . . . . . . 75
6.5. Resolución de Problemas Utilizando Tipos Compuestos. Agenda . . . . . . . . . . 78
7. Búsqueda y Ordenación 85
7.1. Algoritmos de Búsqueda . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85
7.1.1. Búsqueda Lineal (Secuencial) . . . . . . . . . . . . . . . . . . . . . . . . . . 86
7.1.2. Búsqueda Binaria . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 88
7.2. Algoritmos de ordenación . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 89
7.2.1. Ordenación por Selección . . . . . . . . . . . . . . . . . . . . . . . . . . . . 89
7.2.2. Ordenación por Intercambio (Burbuja) . . . . . . . . . . . . . . . . . . . . . 90
7.2.3. Ordenación por Inserción . . . . . . . . . . . . . . . . . . . . . . . . . . . . 90
7.3. Aplicación de los Algoritmos de Búsqueda y Ordenación . . . . . . . . . . . . . . . 92
15.Bibliografía 193
Índice 193
En este manual se describen las características básicas del lenguaje C++. Está concebido desde
el punto de vista docente, por lo que nuestra intención no es hacer una descripción completa
del lenguaje, sino únicamente de aquellas características adecuadas como base para facilitar el
aprendizaje en un primer curso de programación. Se supone que el alumno compatibilizará el uso
de este manual con las explicaciones del profesor, impartidas en el aula.
El lenguaje de programación C++ es un lenguaje muy flexible y versátil. Debido a ello, si
se utiliza sin rigor puede dar lugar a construcciones y estructuras de programación complejas,
difíciles 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 Computación de la
Universidad de Málaga
La última versión de este documento puede ser descargada desde la siguiente página web:
http://www.lcc.uma.es/%7Evicente/docencia/index.html
7
8 CONTENIDO
Programación Básica
9
Capítulo 1
Un Programa C++
En general, un programa C++ suele estar escrito en diferentes ficheros. Durante el proceso de
compilación estos ficheros serán combinados adecuadamente y traducidos a código objeto, obte-
niendo el programa ejecutable. Nosotros comenzaremos tratando con programas sencillos, para los
que bastará un único fichero cuya extensión será una de las siguientes: “.cpp”, “.cxx”, “.cc”, etc.
En el capítulo 10 comenzaremos a estudiar cómo estructurar programas complejos en diferentes
ficheros.
En este capítulo nos centraremos en presentar los elementos imprescindibles y en mostrar cómo
trabajar con el fichero que contiene el programa para generar su correspondiente fichero ejecutable.
En posteriores capítulos trataremos con detalle cada uno de los elementos que puede contener un
programa.
El fichero suele comenzar con unas líneas para incluir las definiciones de los módulos de bi-
blioteca que utilice nuestro programa, e irá seguido de declaraciones y definiciones de tipos, de
constantes y de subprogramas. El programa debe contener un subprograma especial (la función
main) que indica dónde comienza la ejecución. Las instrucciones contenidas en dicha función main
se ejecutarán una tras otra hasta llegar a su fin. La función 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 explícitamente una sentencia return, por defecto, se devolverá un valor que indica
terminación normal (0).
A continuación, mostramos un programa que convierte una cantidad determinada de euros a
su valor en pesetas y describimos cómo hay que proceder para obtener el programa ejecutable
correspondiente. Más 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 básicas: un editor con el que
crear un archivo con el texto del mismo (el código fuente) y un compilador para traducir el código
fuente a código ejecutable. Para realizar estos pasos podemos seguir dos enfoques diferentes: usar
directamente la línea de comandos del sistema operativo o usar un entorno integrado de desarrollo
(IDE).
11
12 CAPÍTULO 1. UN PROGRAMA C++
Si optamos por seguir los pasos desde la línea de comandos (en un entorno linux) podemos
usar cualquier editor de texto (emacs, vi, gedit, kate, etc.) para crear un fichero (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 compilación pueden aparecer errores, que habrá que solucionar. Cada vez
que modifiquemos el fichero para corregir errores deberemos volver a compilar, hasta que no quede
ningún error. En ese momento ya tenemos un fichero ejecutable (denominado euros) y podemos
proceder a su ejecución como se muestra a continuación, donde el texto enmarcado corresponde a
una entrada de datos por parte del usuario:
Bibliotecas
El lenguaje C++ consta de un reducido número de instrucciones, pero ofrece un amplio reper-
torio de bibliotecas con herramientas que pueden ser importadas por los programas cuando
son necesarias. Por este motivo, un programa suele comenzar por tantas líneas #include
como bibliotecas se necesiten. Como se puede observar, en nuestro ejemplo se incluye la bi-
blioteca 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 capítulo 10). Por ahora nos basta con recordar que
nuestros programas deben contener la siguiente directiva:
Identificadores
Para cada elemento que introduzcamos en nuestro programa debemos definir un nombre
(identificador) con el que hacer referencia al mismo y disponer de algún mecanismo para in-
formar al compilador de dicho nombre y de sus características. Hemos visto cómo hacerlo para
constantes simbólicas, pero de igual forma habrá que proceder con otro tipo de elementos,
como variables, tipos, subprogramas, etc, que iremos tratando en posteriores capítulos.
En C++ se considera que las letras minúsculas son caracteres diferentes de las letras mayús-
culas. Como consecuencia, si en nuestro ejemplo hubiéramos usado el identificador eur_pts
el compilador consideraría 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 línea el identificador
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.
Un identificador debe estar formado por una secuencia de letras y dígitos en la que el primer
carácter debe ser una letra. El carácter ’_’ se considera como una letra, sin embargo, los
nombres que comienzan con dicho carácter se reservan para situaciones especiales, por lo que
no deberían utilizarse en programas. Aunque el lenguaje no impone ninguna restricción adi-
cional, en este manual seguiremos unos criterios de estilo a la hora decidir qué identificador
utilizamos para cada elemento. Ello contribuye a mejorar la legibilidad del programa. Por
ejemplo, los identificadores utilizados como nombres de constantes simbólicas estarán forma-
dos por letras mayúsculas y, en caso de querer que tengan más de una palabra, usaremos ’_’
como carácter de separación (EUR_PTS). En posteriores capítulos iremos proporcionando otros
criterios de estilo para ayudar a construir identificadores para variables, tipos, funciones, etc.
Palabras reservadas
Algunas palabras tienen un significado especial en el lenguaje y no pueden ser utilizadas
con otro sentido. Por este motivo no pueden ser utilizados para designar elementos definidos
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 símbolos que indican comienzo o fin de una entidad (( ) { } ; , < >). Por ejemplo, en
nuestro programa euros.cpp usamos { y } para delimitar el comienzo y el final de la función
main, y el símbolo ; para delimitar el final de una sentencia.
Operadores
Son símbolos con significado propio según el contexto en el que se utilicen. Ejemplo: = << >>
* / % + - < > <= >= == != ++ -- . , etc.
• Comentarios hasta fin de línea: los símbolos // marcan el comienzo del comentario, que
se extiende hasta el final de la línea.
//- fichero: euros.cpp --------------------------------------------
Tipos Simples
Un programa trabaja con datos con unas determinadas características. No es lo mismo procesar
números naturales, números reales o nombres (cadenas de caracteres). En cada caso tratamos con
datos de tipo diferente. C++ dispone de un número reducido de tipos predefinidos 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 mani-
pular datos numéricos, posiblemente con decimales. También es posible definir nuevos tipos, con
características no predefinidas sino pensadas específicamente para un determinado programa.
La elección de un determinado tipo u otro para una determinada entidad del programa pro-
porciona información acerca de:
Por ejemplo, al especificar que un dato es de tipo double estamos indicando que se trata de
un número real, representado internamente en punto flotante con doble precisión (normalmente
usando 8 bytes) y que se puede manipular con los operadores predefinidos +, -, * y /.
Los tipos se pueden clasificar 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 predefinidos para acceder a sus dígitos individualmente
(aunque esto se pueda realizar indirectamente mediante operaciones de bits o aritméticas). Por el
contrario, los tipos compuestos (ver el capítulo 6) están formados como un agregado o composición
de otros tipos, ya sean simples o compuestos.
15
16 CAPÍTULO 2. TIPOS SIMPLES
El tipo int se utiliza para trabajar con números enteros. Su representación suele coincidir con
la definida por el tamaño de palabra del procesador sobre el que va a ser ejecutado, hoy día suele
ser de 4 bytes (32 bits), aunque en determinados ordenadores puede ser de 8 bytes (64 bits).
Tanto el tipo float como el double se utilizan para representar números reales en formato de
punto flotante. Se diferencian en el rango de valores que se utiliza para su representación interna.
El tipo double (“doble precisión”) se suele representar utilizando 8 bytes ([64 bits]), mientras que
el tipo float (“simple precisión”) se suele representar utilizando 4 bytes [32 bits]). El tipo double
también puede ser modificado con long para representar “cuádruple precisión” (normalmente 12
bytes [96 bits]).
El tipo bool se utiliza para representar valores lógicos (o booleanos), es decir, los valores
“Verdadero” o “Falso” o las constantes lógicas true y false. Suele almacenarse en el tamaño de
palabra más pequeño posible direccionable (normalmente 1 byte).
El tipo char se utiliza para representar caracteres, es decir, símbolos alfanuméricos (dígitos
y letras mayúsculas y minúsculas), de puntuación, espacios, control, etc. Normalmente utiliza un
espacio de almacenamiento de 1 byte (8 bits) y puede representar 256 valores diferentes (véase 2.10).
Todos los tipos simples tienen la propiedad de ser indivisibles y además, mantener una relación
de orden entre sus elementos (se les pueden aplicar los operadores relacionales, véase 2.5). Se les
conoce también como tipos escalares. Todos ellos, salvo los de punto flotante (float y double),
tienen también la propiedad de que cada posible valor tiene un único antecesor y un único sucesor.
A éstos se les conoce como tipos ordinales (en terminología C++, también se les conoce como
tipos integrales, o enteros). La siguiente tabla muestra las características fundamentales de los
tipos simples predefinidos.
enum Color {
ROJO,
AZUL,
AMARILLO
} ;
Constantes
A su vez, las constantes pueden aparecer como constantes literales, aquellas cuyo valor aparece
directamente en el programa, y como constantes simbólicas, aquellas cuyo valor se asocia a un
identificador, a través del cual se representa.
Ejemplos de constantes literales:
Constantes lógicas (bool):
false, true
Así mismo, ciertos caracteres constantes tienen un significado especial (caracteres de escape):
Constantes enteras, pueden ser expresadas en decimal (base 10), hexadecimal (base 16) y
octal (base 8). El sufijo L se utiliza para especificar long, el sufijo LL se utiliza para es-
pecificar long long, el sufijo U se utiliza para especificar unsigned, el sufijo UL especifica
unsigned long, y el sufijo ULL especifica unsigned long long:
123, -1520, 2345U, 30000L, 50000UL, 0x10B3FC23 (hexadecimal), 0751 (octal)
Constantes reales, números en punto flotante. El sufijo F especifica float, y el sufijo L espe-
cifica long double:
3.1415, -1e12, 5.3456e-5, 2.54e-1F, 3.25e200L
Constantes Simbólicas
Para declarar una constante simbólica se usa la palabra reservada const, seguida por su tipo,
el nombre simbólico (o identificador) con el que nos referiremos a ella y el valor asociado tras
el símbolo (=). Usualmente se definen al principio del programa, después de la inclusión de las
cabeceras de las bibliotecas. Ejemplos de constantes simbólicas:
Variables
Las variables se definen mediante sentencias en las que se especifica el tipo y una lista de
identificadores separados por comas. Todos los identificadores de la lista serán variables de dicho
tipo. En dicha sentencia es posible asignar un valor inicial a una variable. Para ello, usamos el
símbolo = seguido de una expresión con la que se calcula el valor inicial de la variable. En caso de
que no se asigne ningún valor inicial, la variable tendrá un valor inespecificado.
El valor de una variable podrá cambiar utilizando sentencia de asignación (véase 2.6) o mediante
una sentencia de entrada de datos (véase 3.3).
2.5. Expresiones
Un programa se basa en la realización de una serie de cálculos con los que se producen los
resultados deseados. Dichos resultados se almacenan en variables y a su vez son utilizados para
nuevos cálculos, hasta obtener el resultado final. Los cálculos se producen mediante la evaluación
de expresiones en las que se mezclan operadores con operandos (constantes literales, constantes
simbólicas o variables).
En caso de que en una misma expresión se utilice más 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 continuacción mostramos los operadores disponibles (ordena-
dos de mayor a menor precedencia), aunque por ahora únicamente utilizaremos los más simples e
intuitivos:
Operador Tipo de Operador Asociatividad
[] -> . Binarios Izq. a Dch.
! ~ - * Unarios Dch. a Izq.
* / % Binarios Izq. a Dch.
+ - Binarios Izq. a Dch.
<< >> Binarios Izq. a Dch.
< <= > >= Binarios Izq. a Dch.
== != Binarios Izq. a Dch.
& Binario Izq. a Dch.
^ Binario Izq. a Dch.
| Binario Izq. a Dch.
&& Binario Izq. a Dch.
|| Binario Izq. a Dch.
?: Ternario Dch. a Izq.
Aritméticos. El resultado es del mismo tipo que los operandos (véase 2.8):
Operadores de bits, sólo aplicable a operandos de tipos enteros. El resultado es del mismo
tipo que los operandos:
cond ? valor1 : valor2 Si cond es true entonces el resultado es valor1, en otro caso es valor2
Lógicos, sólo aplicable a operandos de tipo booleano. Tanto el operador && como el operador
|| se evalúan en cortocircuito. El resultado es de tipo bool:
! valor Negación lógica (Si valor es true entonces false, en otro caso true)
valor1 && valor2 AND lógico (Si valor1 es false entonces false, en otro caso valor2)
valor1 || valor2 OR lógico (Si valor1 es true entonces true, en otro caso valor2)
x ! x x y x && y x || y
F T F F F F
T F F T F T
T F F T
T T T T
• Los operadores && y || se evalúan en cortocircuito, que significa que cuando ya se conoce el
resultado de la operación lógica trás la evaluación del primer operando, entonces el segundo
operando no se evalúa.
◦ Para el operador &&, cuando el primer operando se evalúa a false, entonces el resultado
de la operación && es false sin necesidad de evaluar el segundo operando.
◦ Para el operador ||, cuando el primer operando se evalúa a true, entonces el resultado de
la operación || es true sin necesidad de evaluar el segundo operando.
Sin embargo, si trás la evaluación del primer operando no se puede calcular el resultado de la
operación lógica, entonces sí es necesario evaluar el segundo operando para calcular el resultado
de la expresión lógica.
◦ Para el operador &&, cuando el primer operando se evalúa a true, entonces el resultado de
la operación && es el resultado de evaluar el segundo operando.
◦ Para el operador ||, cuando el primer operando se evalúa a false, entonces el resultado
de la operación || es el resultado de evaluar el segundo operando.
<variable> = <expresión> ;
Por ejemplo:
v = v + 1 ;
aunque también es posible obtener el mismo resultado con ++v; o v++;. Nótese que aunque en
realidad ambas sentencias dan el mismo resultado, en realidad no son equivalentes. En este curso
usaremos las sentencias de asignación de forma que siempre serán equivalentes. Hay otros opera-
dores abreviados de asignación, que mostramos en la siguiente tabla:
Sentencia Equivalencia
++variable; variable = variable + 1;
--variable; variable = variable - 1;
variable++; variable = variable + 1;
variable--; variable = variable - 1;
{
cnt = 0 ;
int cnt ;
}
Promoción: en cualquier operación 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:
enum Color {
ROJO, AZUL, AMARILLO
} ;
int main()
{
Color c = ROJO ;
c = Color(c + 1) ;
// ahora c tiene el valor AZUL
}
Rep Simb Rep Simb Rep Simb Rep Simb Rep Simb Rep Simb Rep Simb Rep Simb
0 \0 16 DLE 32 SP 48 0 64 @ 80 P 96 ‘ 112 p
1 SOH 17 DC1 33 ! 49 1 65 A 81 Q 97 a 113 q
2 STX 18 DC2 34 " 50 2 66 B 82 R 98 b 114 r
3 ETX 19 DC3 35 # 51 3 67 C 83 S 99 c 115 s
4 EOT 20 DC4 36 $ 52 4 68 D 84 T 100 d 116 t
5 ENQ 21 NAK 37 % 53 5 69 E 85 U 101 e 117 u
6 ACK 22 SYN 38 & 54 6 70 F 86 V 102 f 118 v
7 \a 23 ETB 39 ’ 55 7 71 G 87 W 103 g 119 w
8 \b 24 CAN 40 ( 56 8 72 H 88 X 104 h 120 x
9 \t 25 EM 41 ) 57 9 73 I 89 Y 105 i 121 y
10 \n 26 SUB 42 * 58 : 74 J 90 Z 106 j 122 z
11 \v 27 ESC 43 + 59 ; 75 K 91 [ 107 k 123 {
12 \f 28 FS 44 , 60 < 76 L 92 \ 108 l 124 |
13 \r 29 GS 45 - 61 = 77 M 93 ] 109 m 125 }
14 SO 30 RS 46 . 62 > 78 N 94 ^ 110 n 126 ~
15 SI 31 US 47 / 63 ? 79 O 95 _ 111 o 127 DEL
#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 ;
}
25
26 CAPÍTULO 3. ENTRADA Y SALIDA DE DATOS BÁSICA
#include <iostream>
#include <iomanip>
using namespace std ;
int main()
{
bool x = true ;
cout << boolalpha << x ; // escribe los booleanos como ’false’ o ’true’
El manipulador boolalpha especifica que los valores lógicos se mostrarán mediante los valores
false y true. Si no se especifica, se muestran los valores 0 y 1 respectivamente.
Los manipuladores dec, hex, oct especifican que la salida se realizará utilizando el sistema
de numeración decimal, hexadecimal u octal respectivamente.
El manipulador setw(...) especifica la anchura (width) que como mínimo ocupará la salida
da datos (permite mostrar la información de forma tabulada).
incluso es posible leer varios valores consecutivamente en la misma sentencia de entrada, de tal
forma que las siguientes sentencias son equivalentes:
El operador de entrada >> se comporta de la siguiente forma: elimina los espacios en blanco1 que
hubiera al principio del flujo de entrada de datos, y lee de dicho flujo de entrada una secuencia de
caracteres hasta que encuentre algún carácter no válido (según el tipo de la variable que almacenará
la entrada de datos), que no será leído y permanecerá disponible en el flujo de entrada de datos
hasta que se realice la próxima operación de entrada. La secuencia de caracteres leída del flujo de
entrada será manipulada (convertida) para obtener el valor correspondiente del tipo adecuado que
será almacenado en la variable especificada.
En caso de que durante la operación de entrada de datos surja alguna situación de error, dicha
operación de entrada se detiene y el flujo de entrada se pondrá en un estado erróneo.
Por ejemplo, dado el siguiente programa:
#include <iostream>
using namespace std ;
int main()
{
int num_1, num_2 ;
cout << "Introduce el primer número: " ;
cin >> num_1 ;
cout << "Introduce el segundo número: " ;
cin >> num_2 ;
cout << "Multiplicación: " << (num_1 * num_2) << endl ;
cout << "Fin" << endl ;
}
si al ejecutarse, el usuario introduce 12 enter como primer número, y 3 enter como segundo
número, se produce la siguiente salida:
Multiplicación: 36
Fin
En caso de querer eliminar los espacios iniciales explícitamente, el manipulador ws realizará dicha
operación de eliminación de los espacios iniciales:
{
char c ;
cin >> ws ; // elimina los espacios en blanco iniciales
Es posible también eliminar un número determinado de caracteres del flujo de entrada, o hasta
que se encuentre un determinado carácter:
{
cin.ignore() ; // elimina el próximo carácter
cin.ignore(5) ; // elimina los 5 próximos caracteres
cin.ignore(1000, ’\n’) ; // elimina 1000 caracteres o hasta ENTER (nueva-línea)
}
Sin embargo, es posible comprobar el estado de un determinado flujo 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 y 9: ";
cin >> n;
while (cin.fail()) { // ¿ Estado Erróneo ?
cin.clear(); // Restaurar estado
cin.ignore(1000, ’\n’); // Eliminar la entrada de datos anterior
cout << "Error: Introduzca un numero 1 y 9: ";
cin >> n;
}
} while (! (n > 0 && n < 10));
cout << "Valor: " << n << endl;
}
Estructuras de Control
El lenguaje de programación C++ dispone de estructuras de control muy flexibles. Aunque ello
es positivo, un abuso de la flexibilidad proporcionada por el lenguaje puede dar lugar a programas
con estructuras complejas y características no deseables. Por ello, sólo veremos algunas de ellas
y las utilizaremos en contextos y situaciones restringidas. Todas aquellas estructuras que no se
presenten en este texto no serán utilizadas en el curso.
31
32 CAPÍTULO 4. ESTRUCTURAS DE CONTROL
de entidades se crean al principio del programa y se destruyen al finalizar éste. Por ejemplo, la
constante simbólica EUR_PTS, utilizada en nuestro primer programa, es global y visible (utilizable)
desde el punto en que se declaró hasta el final del fichero. En capítulos posteriores estudiaremos
otras entidades que se declaran con un ámbito global, como tipos definidos por el programador
(ya hemos visto tipos enumerados), prototipos de subprogramas y definiciones de subprogramas.
También es posible definir variables con un ámbito global, aunque es una práctica que, en general,
está desaconsejada y no seguiremos en este curso.
Entidades locales son aquellas que se definen dentro de un bloque. Su ámbito de visibilidad
comprende desde el punto en el que se definen hasta el final de dicho bloque. Este tipo de entidades
se crean en el punto donde se realiza la definición, y se destruyen al finalizar el bloque. Normalmente
serán variables locales, aunque también es posible declarar constantes localmente. Sin embargo, por
lo general en este curso seguiremos el criterio de declarar las constantes globalmente escribiendo
su definición al principio del programa, fuera de cualquier bloque.
#include <iostream>
using namespace std ;
const double EUR_PTS = 166.386 ; // Declaración de constante GLOBAL
int main()
{
cout << "Introduce la cantidad (en euros): " ;
double euros ; // Declaración de variable LOCAL
cin >> euros ;
double pesetas = euros * EUR_PTS ; // Declaración de variable LOCAL
cout << euros << " Euros equivalen a " << pesetas << " Pts" << endl ;
}
conversión u otra. Para poder describir este comportamiento necesitamos sentencias de selección,
que permitan efectuar preguntas y seleccionar el comportamiento adecuado en función del resultado
de las mismas. Las preguntas serán expresadas mediante expresiones lógicas, que devuelven un valor
de tipo bool. El valor resultante de evaluar la expresión lógica (true o false), será utilizado para
seleccionar el bloque de sentencias a ejecutar, descartando el resto de alternativas posibles.
Sentencia if
La sentencia de selección se puede utilizar con diferentes formatos. Para el ejemplo mencionado
usaríamos una sentencia de selección condicional compuesta:
cuya ejecución comienza evaluando la expresión lógica. Si su resultado es verdadero (true) enton-
ces 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 ejecución continúa por
la siguiente intruccción después de la sentencia if.
De esta forma, podríamos 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 selección simple, que responde al siguiente esquema:
true
Cond
if ( <expresión_lógica> ) {
<secuencia_de_sentencias> ; false
Acciones
}
cuya ejecución comienza evaluando la expresión lógica. Si su resultado es verdadero (true), entonces
se ejecuta la secuencia de sentencias entre llaves; si es falso (false), entonces no se ejecuta ninguna
sentencia. Como ejemplo, a continuación mostramos un programa que lee tres números 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 selección:
true
if ( <expresión_lógica_1> ) { Cond Acciones
<secuencia_de_sentencias_v1> ; false
Sentencia switch
Aunque el uso de la sentencia if podría ser suficiente para seleccionar flujos de ejecución alter-
nativos, hay numerosas situaciones que podrían quedar mejor descritas utilizando otras estructuras
de control que se adapten mejor. Por ejemplo, supongamos que queremos ampliar nuestro progra-
ma de conversión de moneda para ofrecer un menú de opciones que permita convertir diferentes
monedas (pesetas, francos, marcos y liras) a euros. En este caso se podría haber optado por usar
una sentencia de selección múltiple, 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 de Pesetas a Euros" ;
cout << "Teclee F para convertir de Francos a Euros" ;
cout << "Teclee M para convertir de Marcos a Euros" ;
cout << "Teclee L para convertir de Liras a Euros" ;
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 las anteriores es a Liras
result = cantidad * EUR_LIR ;
}
cout << cantidad << " equivale a " << result << endl ;
}
switch ( <expresión> ) {
case <valor_cte_1> :
<secuencia_de_sentencias_1> ;
break ;
case <valor_cte_2> :
case <valor_cte_3> :
<secuencia_de_sentencias_2> ; Expr
break ; case_1 case_2 ... case_n default
#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 de Pesetas a Euros" ;
cout << "Teclee F para convertir de Francos a Euros" ;
cout << "Teclee M para convertir de Marcos a Euros" ;
cout << "Teclee L para convertir de Liras a Euros" ;
cout << "Opcion: " ;
cin >> resp ;
cout << "Introduce la cantidad : " ;
double cantidad, result ;
cin >> cantidad ;
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’:
cout << cantidad << " equivale a " << result << endl ;
}
Aunque aparentemente la versión con if es similar a la versión con switch, en realidad esta
segunda versión es más clara. Cualquier persona que lea el programa podrá suponer que su intención
es seleccionar una alternativa u otra en función del valor resultado de evaluar una determinada
expresión, mientras que para llegar a la misma conclusión en la versión con if es necesario leer
una a una todas las expresiones que seleccionan la entrada en cada rama.
Sentencia while
Comenzamos mostrando la sentencia while, la más general, que responde al siguiente esquema:
false
Cond
while ( <expresión_lógica> ) {
true
<secuencia_de_sentencias> ;
} Acciones
Su ejecución comienza con la evaluación de la expresión lógica. Si es falsa, el cuerpo del bucle no
se ejecuta y el flujo de ejecución pasa directamente a la instrucción que siga a la sentencia while.
Si es cierta, el flujo de ejecución pasa al cuerpo del bucle, el cual se ejecuta completamente en
secuencia y posteriormente vuelve a evaluar la expresión lógica. Este ciclo iterativo consistente en
evaluar la condición y ejecutar las sentencias se realizará MIENTRAS que la condición se evalúe
a verdadero y finalizará cuando la condición se evalúe a falso.
Por ejemplo, supongamos que queremos que nuestro programa de conversión se ejecute repe-
tidamente hasta que el usuario teclee ’N’ como respuesta a la operación a realizar. En ese caso
podríamos utilizar el siguiente programa:
#include <iostream>
using namespace std ;
const double EUR_PTS = 166.386 ;
int main()
{
char resp ;
A continuación, mostramos otro ejemplo en el que, dado un número tomado como entrada,
escribimos en pantalla el primer divisor mayor que 1. Si el número 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 número de iteraciones del bucle depende de cuando
se evalúe a false la expresión lógica que, a su vez, depende de variables utilizadas en el bucle. Por
ello, habrá que considerar qué variables utilizamos en la expresión de control y de qué forma se
comportan. Para que el bucle se ejecute exactamente el número de iteraciones que nos interesa, es
importante que nos aseguremos de que dichas variables tienen los valores adecuados inicialmente
y que estudiemos cómo son modificadas en el cuerpo del bucle. Si las variables nunca fueran
modificadas en el cuerpo tendríamos un bucle infinito, porque la condición de control nunca se
cumpliría y por tanto el programa nunca terminaría.
Sentencia for
Frecuentemente nos encontramos con situaciones en las que el número de iteraciones que desea-
mos que ejecute el bucle es previsible y puede ser controlado utilizando una variable de control.
En estos casos la solución queda más clara si utilizamos un tipo de bucle que permita conocer
fácilmente qué variable se utiliza para controlar cuando acaba la ejecución, y cómo va evolucio-
nando. Para ello, en C++ se utiliza la sentencia for, similar a la de otros lenguajes como Pascal
o Modula-2, pero mucho más flexible, por lo que habrá que utilizarla aprovechando su flexibilidad
pero sin abusar, porque ello podría producir programas inadecuados.
En realidad la sentencia for puede ser vista como una construcción especializada de la sentencia
while presentada anteriormente, pero con una sintaxis diferente para hacer más explícito los casos
en los que la iteración está controlada por los valores que toma una determinada variable de control,
de tal forma que existe una clara inicialización y una clara modificación (incremento) de la variable
de control, hasta llegar al caso final. La sentencia for sigue el siguiente esquema:
<inicialización> ; Acciones
while ( <expresión_lógica> ) {
<secuencia_de_sentencias> ;
Incremento
<incremento> ;
}
Para utilizar adecuadamente la estructura for es necesario que el comportamiento iterativo del
bucle quede claramente especificado utilizando únicamente los tres componentes (inicialización,
condición de fin 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 modificar 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 inicialización. En este caso especial, el ámbito de visibilidad de la variable de control del bucle
es solamente hasta el final del bloque de la estructura for.
Como ejemplo, a continuación mostramos un programa que toma como entrada un número n
y muestra por pantalla la serie de números 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 cómo la sentencia for puede ser considerada como una especialización de la
sentencia while, por lo que el programa anterior también podría 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 ;
}
Ambas soluciones son correctas y a primera vista puede parecer que de similar claridad. Sin
embargo, la primera solución es mejor que la segunda: en el primer caso toda la información
necesaria para analizar el comportamiento del bucle está claramente localizable, mientras que en
el segundo es necesario localizarla estudiando cómo se inicializa la variable y cómo se incrementa.
Aunque en este ejemplo ello está claro, piense que en otras situaciones esta información puede estar
mucha más oculta y, por tanto, ser más difícil de obtener.
Sentencia do-while
Finalmente, se dispone de un tercer tipo de sentencia de iteración, la sentencia do-while. Al
igual que ocurre con la sentencia for, esta sentencia puede ser vista como una construcción espe-
cializada para determinadas situaciones que, aunque admiten una solución en base a la sentencia
while, presentan una solución más clara con do-while. La sentencia do-while sigue el siguiente
esquema:
do { Acciones
<secuencia_de_sentencias> ;
} while ( <expresión_lógica> ) ; Cond
true
false
En este caso, a diferencia de la sentencia while, en la que primero se evalúa la expresión lógica
y después, 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 evalúa la expresión lógica y, si
ésta es cierta, se repite el proceso. En este caso, el flujo de ejecución alcanza la expresión lógica
después 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 diseñar un programa que lea un número y se asegure de que es par, repitiendo
la lectura en caso de no serlo, podríamos hacer lo siguiente:
#include <iostream>
using namespace std;
int main ()
{
int num ;
do {
cout << "Tecle un número par: " ;
cin >> num ;
} while ((num % 2) != 0) ;
cout << "El número par es " << num << endl ;
}
Como hemos mencionado, también sería posible obtener una versión alternativa de este pro-
grama haciendo uso de una sentencia while, de la siguiente forma:
#include <iostream>
using namespace std;
int main ()
{
int num ;
cout << "Tecle un número par: " ;
cin >> num ;
cout << "El número par es " << num << endl ;
}
Sin embargo, como puede observarse, la primera versión 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 iteración que ya lo expresa? El segundo programa se basa en sacar una iteración
fuera del bucle y utilizar un tipo de bucle que se repite cero o más veces. Aunque el comportamiento
es equivalente, la solución parece un poco forzada.
Inicializac
false
Cond
true
false
Acciones Cond
Acciones
true
Incremento Acciones
Cond
true
false
Para que la aplicación del principio sea factible, es necesario que el programador pueda confiar
en que cada estructura tiene un único punto de entrada y de salida. Ello es especialmente importante
cuando se trabaja con subprogramas (ver el capítulo 5), pero también es importante al trabajar
con las estructuras básicas presentadas en este capítulo. Para aclarar este punto volveremos a
repasar la sentencia de iteración for. Sabemos que su gran ventaja está en que permite escribir en
una misma zona del programa toda la información necesaria para saber cómo evoluciona el bucle.
Ello es así porque dentro de la misma sentencia se encuentra la variable de control utilizada en el
mismo, su inicialización y su modificación, después de cada iteración. El programador puede leer
esta zona del programa y conocer cuántas iteraciones va a dar el bucle, utilizando la abstracción,
ignorando el cuerpo del bucle. En realidad, ello es así si estamos seguros de que el programador no
hace ninguna modificación de la variable de control dentro del cuerpo del bucle. Si el programador
admite la posibilidad de que la variable de control sea modificada dentro del cuerpo del bucle,
4.6. Ejemplos
A continuación, repasaremos los diferentes elementos introducidos en este capítulo viendo al-
gunos ejemplos.
#include <iostream>
using namespace std ;
int main ()
{
cout << "Introduzca dos números: " ;
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 continuación queremos obtener un programa para calcular el factorial de un número 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 número de iteraciones a realizar y un procesamiento
acumulativo, en el que en cada iteración multiplicamos por un valor diferente. En este caso, como
también conocemos las iteraciones que queremos que ejecute el bucle, volvemos a usar una sentencia
for, pero en esta ocasión 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. Nótese que en esta ocasión utilizamos el valor de la variable de control dentro del cálculo
iterativo.
#include <iostream>
using namespace std ;
int main ()
{
cout << "Introduzca un número: " ;
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 ;
}
#include <iostream>
using namespace std ;
int main ()
{
cout << "Introduzca dos números: " ;
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 modificar el programa anterior para que siempre sea posible realizar la división. Para
ello, modifique 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.
Subprogramas. Funciones y
Procedimientos
El diseño de un programa es una tarea compleja, por lo que deberemos abordarla siguiendo
un enfoque que permita hacerla lo más simple posible. Al igual que ocurre en otros contextos, la
herramienta fundamental para abordar la solución de problemas complejos es la abstracción: una
herramienta mental que nos permite tratar el problema identificando sus elementos fundamentales
y dejando para más adelante el estudio de los detalles secundarios. La aplicación de este principio
al desarrollo de programas permite seguir un enfoque de refinamientos sucesivos: en cada fase del
diseño 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 final tenemos un diseño completo, obtenido con menor esfuerzo y de forma más
segura.
Los lenguajes de programación ofrecen la posibilidad de definir subprogramas, permitiendo al
programador aplicar explícitamente la abstracción en el diseño y construcción 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 también pueden ser vistos como una
ampliación del conjunto de operaciones básicas del lenguaje de programación, proporcionándole
nuevas herramientas no disponibles de forma predefinida.
45
46 CAPÍTULO 5. SUBPROGRAMAS. FUNCIONES Y PROCEDIMIENTOS
int main()
{
int x = 8 ;
int y = 4 ;
ordenar(x, y) ;
cout << x << " " << y << endl ;
}
Como vemos, la función main se comunica con el subprograma ordenar mediante una llama-
da. En esta comunicación hay un intercambio de información entre main y ordenar a través
de los parámetros utilizados en la llamada, en este caso las variables x e y.
Funciones: encargadas de realizar un cálculo computacional y generar un único resultado.
Las funciones responden a los mismos principios que los procedimientos, salvo que están
especializados para que la comunicación entre el que hace la invocación y la función llamada
tenga lugar de una forma especial, que se adapta muy bien y es muy útil en numerosas
situaciones. En el siguiente ejemplo, la función calcular_menor recibe dos números como
parámetros y calcula el menor de ellos. En este caso la comunicación entre el que hace la
llamada (la función main) y la función llamada se hace de forma diferente. Antes hicimos
la invocación en una instrucción independiente, sin embargo, ahora se hace como parte de
una instrucción más compleja. Ello es así porque una función devuelve un valor (en este
caso el menor número) y dicho valor deberá ser utilizado como parte de un cálculo más
complejo. En nuestro ejemplo, como resultado de la invocación obtendremos un número que
será almacenado en una variable de la función main.
int main()
{
int x = 8 ;
int y = 4 ;
int z = calcular_menor(x, y) ;
cout << "Menor: " << z << endl ;
}
En los ejemplos planteados vemos que tanto procedimientos como funciones se utilizan para
realizar un cálculo, ignorando los detalles y, por tanto, simplificando el diseño del programa prin-
cipal. En ambos casos hay una invocación al subprograma correspondiente y un intercambio de
información 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 información es a través de los parámetros usados en la llamada.
La llamada a una función no constituye por sí sola una sentencia, por lo que debe aparecer
dentro de alguna sentencia que utilice el valor resultado de la función. La información se
intercambia a través de los parámetros y mediante el valor devuelto por la función.
programa que la haga visible en el punto en que se usen. Por ahora situaremos la definición de los
subprogramas antes de su uso, aunque en la sección 5.7 mostraremos otras posibilidades.
Hemos comentado que un subprograma es como un mini programa encargado de resolver un
subproblema, por lo que la definición de un subprograma no difiere de la definición ya utilizada de
la función main, que en realidad no es más que una función especial. A continuación se muestra un
programa completo con la definición de la función main, que define el comportamiento del programa
principal, y las definiciones de los dos subprogramas ordenar y calcular_menor, utilizados desde
la función 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 ;
}
1. Se establecen las vías de comunicación entre los algoritmos llamante y llamado por medio de
los parámetros.
2. Posteriormente, el flujo de ejecución pasa a la primera sentencia del cuerpo del subprograma
llamado, cuyas instrucciones son ejecutadas secuencialmente, en el orden en que están escritas.
3. Cuando sea necesario, se crean las variables locales especificadas en el cuerpo del subprogra-
ma.
4. Cuando finaliza la ejecución del subprograma, las variables locales y los parámetros pre-
viamente creados se destruyen, el flujo de ejecución retorna al (sub)programa llamante, y
continúa la ejecución por la sentencia siguiente a la llamada realizada.
Serán parámetros de entrada aquellos que se utilizan para recibir la información necesaria para
realizar una computación. Por ejemplo, los parámetros a y b de la función calcular_menor
anterior.
Los parámetros de entrada se definen mediante paso por valor (cuando son de tipos simples1 ).
Ello significa que los parámetros formales son variables independientes, que toman sus valores
iniciales como copias de los valores de los parámetros actuales de la llamada en el momento
de la invocación al subprograma. El parámetro actual puede ser cualquier expresión cuyo tipo
sea compatible con el tipo del parámetro formal correspondiente. Se declaran especificando
el tipo y el identificador asociado.
más adelante.
Los parámetros de salida se definen mediante paso por referencia. Ello significa que el pará-
metro formal es una referencia a la variable que se haya especificado como parámetro actual
en el momento de la llamada al subprograma. Ello exige que el parámetro actual correspon-
diente a un parámetro formal por referencia sea una variable. Cualquier acción dentro del
subprograma que se haga sobre el parámetro formal se realiza sobre la variable referenciada,
que aparece como parámetro actual en la llamada al subprograma. Se declaran especificando
el tipo, el símbolo “ampersand ” (&) y el identificador asociado.
En el siguiente ejemplo, el procedimiento dividir recibe, en los parámetros de entrada
dividendo y divisor, dos números que queremos dividir para obtener su cociente y resto.
Dichos resultados serán devueltos al punto de la llamada utilizando los parámetros de salida
coc y resto, que son pasados por referencia.
En realidad, dado que los parámetros coc y resto han sido pasados por referencia, una
vez efectuada la llamada éstos quedan asociados a las correspondientes variables usadas
como parámetros actuales (las variables cociente, resto de la función main). Así, cualquier
modificación en los parámetros es, en realidad, una modificación de las variables cociente
y resto de la función main. De esta forma, cuando acaba el subprograma conseguimos que
los resultados estén 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 parámetros de entrada/salida a aquellos que se utilizan tanto para recibir
información de entrada, necesaria para que el subprograma pueda realizar su computación,
como para devolver los resultados obtenidos de la misma. Se definen mediante paso por
referencia y se declaran como se especificó anteriormente para los parámetros de salida.
Por ejemplo, los parámetros 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 con-
tienen los valores a ordenar y, cuando acaba el subprograma, son utilizados para devolver los
resultados. Ello es así porque se definen mediante paso por referencia. De esta forma, cuando
dentro del subprograma trabajamos con los parámetros formales a y b, en realidad accede-
mos a las variables utilizadas en la llamada. Si dentro del subprograma se intercambian los
valores de los parámetros, indirectamente se intercambian también 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 ;
}
}
La siguiente tabla relaciona los diferentes modos de comunicación con la forma de efectuar el
paso de parámetros. Todo lo comentado hasta ahora es aplicable a tipos simples. En el capítulo 6
consideraremos el paso de parámetros para el caso de tipos compuestos.
Tipos
Simples Compuestos
P.Valor P.Ref.Cte
(⇓) Entrada
(int x) (const Persona& x)
P.Ref P.Ref
(⇑) Salida, (m) E/S
(int& x) (Persona& x)
subprograma se traduzca como código en línea en vez de como una llamada a un subprograma.
Para ello se especificará la palabra reservada inline justo antes del tipo. De esta forma, se man-
tiene los beneficios proporcionados por la abstracción, pero se eliminan los costes asociados a la
invocación.
inline int calcular_menor(int a, int b)
{
return (a < b) ? a : b ;
}
Este mecanismo sólo es adecuado cuando el cuerpo del subprograma es muy pequeño, de tal forma
que el coste asociado a la invocación dominaría respecto a la ejecución del cuerpo del mismo.
void imprimir(int x)
{
En el siguiente ejemplo la función media permite calcular la media de dos o de tres números
enteros, dependiendo de cómo se efectúe la llamada.
En C++, las pre-condiciones y post-condiciones se pueden especificar 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 comprobación de asertos mediante la siguiente directiva
de compilación:
Tipos Compuestos
Los tipos compuestos surgen de la composición y/o agregación de otros tipos para formar
nuevos tipos de mayor entidad. Existen dos formas fundamentales de crear tipos de mayor entidad:
la composición de elementos, que denominaremos “Registros” o “Estructuras” y la agregación de
elementos del mismo tipo, que se conocen como “Agregados”, “Arreglos” o mediante su nombre en
inglés “Arrays”. Además de estos tipos compuestos definidos por el programador, los lenguajes de
programación suelen proporcionar algún tipo adicional para representar “cadenas de caracteres”.
55
56 CAPÍTULO 6. TIPOS COMPUESTOS
En estos casos, suele ser más adecuado que el subprograma devuelva el valor de tipo compuesto
como un parámetro de salida mediante el paso por referencia.
Si la definición de una variable de tipo string no incluye la asignación de un valor inicial, dicha
variable tendrá como valor por defecto la cadena vacía ("").
La utilización del operador >> sobre un flujo 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 ;
}
#include <iostream>
#include <string>
using namespace std ;
int main()
{
string nombre ;
cout << "Introduzca el nombre: " ;
getline(cin, nombre) ;
cout << "Nombre: " << nombre << endl ;
}
Además, la función getline permite especificar el delimitador que marca el final de la secuencia
de caracteres a leer. Si no se especifica ninguno (como ocurre en el ejemplo anterior), por defecto
se utiliza el carácter de fin de línea. Sin embargo, si se especifica el delimitador, lee y almacena
todos los caracteres del buffer hasta leer el carácter delimitador especificado, el cual es eliminado
del buffer, 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 carácter.
#include <iostream>
#include <string>
using namespace std ;
1 Se consideran espacios en blanco los siguientes caracteres: espacio en blanco (’ ’), tabuladores (’\t’, ’\v’ y
Como hemos visto, el comportamiento de las operaciones de lectura con >> y getline es
diferente. En ocasiones, cuando se utiliza una lectura con getline después 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 continuación lo veremos sobre un ejemplo.
Supongamos que queremos diseñar 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
numéricos:
#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 esperábamos. La primera
iteración funciona adecuadamente, el flujo de ejecución 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 anómala, ya que la ejecución del programa
no se detiene para que el usuario pueda introducir el nombre.
Esto es debido a que no hemos tenido en cuenta cómo se comportan las operaciones de lectura
de datos (>> y getline) al obtener los datos del buffer de entrada. Hay que considerar que después
de leer la edad en una determinada iteración, en el buffer permanece el carácter de fin de línea
(ENTER) que se introdujo tras teclear la edad, ya que éste no es leído por el operador >>. En la
siguiente iteración, la función getline lee una secuencia de caracteres hasta encontrar un ENTER
(sin saltar los espacios iniciales), por lo que leerá el carácter ENTER que quedó en el buffer en la
lectura previa de la edad de la iteración anterior, haciendo que finalice la lectura directamente. El
resultado es que, al leer el nombre, se lee una cadena vacía, sin necesidad de detener el programa
para que el usuario introduzca el nombre de la persona.
La solución a este problema es eliminar los caracteres de espacios en blanco (y fin de línea) del
buffer de entrada. De esta forma el buffer estará realmente vacío y conseguiremos que la ejecución
de getline haga que el programa se detenga hasta que el usuario introduzca el nombre. Hay
diferentes formas de conseguir que el buffer se quede vacío.
Para eliminar los caracteres de espacios en blanco y fin de línea del buffer de entrada antes
de leer la secuencia de caracteres con getline, utilizaremos el manipulador ws en el flujo cin,
que extrae todos los espacios en blanco hasta encontrar algún carácter distinto, por lo que no será
posible leer una cadena de caracteres vacía. Por ejemplo:
#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 línea
cout << "Introduzca el nombre: " ;
getline(cin, nombre) ;
cout << "Introduzca edad: " ;
cin >> edad ;
cout << "Edad: " << edad << " Nombre: [" << nombre << "]" << endl ;
}
}
También es posible que nos interese que la cadena vacía sea una entrada válida en el programa.
En nuestro ejemplo podríamos estar interesados en que el usuario introduzca un nombre vacío como
respuesta. En este caso, es necesario que el buffer se encuentre vacío en el momento de realizar
la operación de entrada. Para ello, eliminaremos los caracteres que pudiera contener el buffer (no
únicamente espacios en blanco) después de la última operación de lectura de datos, usando la
función 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 función ignore elimina todos los caracteres del buffer de entrada en el flujo especificado,
hasta que se hayan eliminado el número de caracteres indicado en el primer argumento o bien se
haya eliminado el carácter indicado en el segundo.
Nótese que la sentencia cin >> ws se asocia a la función getline que le sigue, mientras que la
sentencia ignore se asocia a la sentencia de entrada >> que le precede.
#include <iostream>
#include <string>
using namespace std ;
const string AUTOR = "José Luis" ;
int main()
{
string nombre = "Pepe" ;
// ...
nombre = AUTOR ;
}
Paso como parámetro a subprogramas. Al igual que ocurre con los valores de otros tipos
compuestos, podremos pasar cadenas como parámetros a subprogramas. Si el parámetro es
de salida, o de entrada/salida, usaremos paso por referencia, y si es de entrada usaremos paso
por referencia constante. Por ejemplo, si quisiéramos leer y escribir valores de tipo string
podríamos declarar los subprogramas:
Cadenas como valor de retorno de funciones. Aunque está permitida la devolución de cadenas
como valor de retorno de funciones, es una operación que está desaconsejada, debido a su
alto coste. Por ello, cuando un subprograma necesite devolver una cadena, usualmente se
devolverá como un parámetro de salida.
Es importante tener en cuenta que el índice utilizado para acceder al carácter de la cadena
debe corresponder a una posición válida de la misma. El acceso fuera del rango válido (por
ejemplo, para añadir caracteres al final) es un error que hay que evitar. Aunque se trata de
un error del programa, durante su ejecución no se nos avisa del mismo3 . A partir de dicho
momento el comportamiento del programa quedaría 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.
2 La
comparación lexicográfica se basa en la ordenación alfabética, comúnmente utilizada en los diccionarios.
3 En
GNU C++ la opción de compilación -D_GLIBCXX_DEBUG permite comprobar los índices de acceso de forma
automática.
6.2.3. Ejemplos
A continuación, mostramos algunos ejemplos de programas en los que trabajamos con cadenas
de caracteres representadas mediante el tipo string.
#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 ;
}
Nótese que un carácter concreto de la cadena es de tipo char, y se pasa por referencia a un
subprograma que convierte una letra a su equivalente en mayúscula.
Suponemos que la palabra introducida es correcta y está formada por letras minúsculas.
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 cuál es el último carácter. Una
vez seleccionado el caso adecuado, procedemos a añadir caracteres a la palabra según corresponda.
Usamos el procedimiento plural_1 en el que se utiliza el operador de concatenación para añadir
la terminación adecuada a la cadena. En caso de ser necesario, se accede a la última letra para
cambiar la ’z’ por ’c’. Nótese que para ello usamos el operador de acceso. Ello es posible porque
dicha letra pertenece a la cadena y lo que queremos es sustituir un carácter existente por otro. Sin
embargo, no es posible utilizar el operador de acceso para añadir la terminación ’s’ al final de la
cadena, porque intentaríamos acceder a un carácter no existente en la misma, generando con ello
un error.
Aunque desde el programa principal se hace uso del procedimiento plural_1, también se mues-
tra una implementación 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 final y al resultado le concatenamos la terminación "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 ;
}
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 función replace, que podría haber sido utilizada
directamente para obtener el objetivo propuesto. Este subprograma es equivalente a la operación
str.replace(i, sz, nueva).
struct Fecha {
unsigned dia ;
unsigned mes ;
unsigned anyo ;
} ;
Una vez definido el tipo registro, podrá ser utilizado como cualquier otro tipo, para definir
constantes simbólicas, variables o parámetros formales en los subprogramas. Por ejemplo, podemos
utilizar el nuevo tipo Fecha para definir variables:
Fecha f_ingreso ;
Los valores de tipo estructurado están formados por diferentes componentes, por lo que ne-
cesitamos alguna notación 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 definición del tipo registro) y utilizando llaves para agruparlo todo.
Los valores del tipo Fecha se componen de tres elementos concretos, el día (de tipo unsigned),
el mes (de tipo unsigned) y el año (de tipo unsigned). Los identificadores dia, mes y anyo
representan los nombres de sus elementos componentes, denominados campos, cuyo ámbito de
visibilidad se restringe a la propia definición del registro. Los campos de un registro pueden ser de
cualquier tipo de datos, simple o compuesto. Por ejemplo, podríamos estar interesados en tratar
con información de empleados, definiendo un nuevo tipo Empleado como un registro que contiene
el nombre del empleado (de tipo string), su código 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 pa-
rámetros) 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 características de dicho tipo, como si se tratase de una variable de
dicho tipo.
int main ()
{ dia: 18 dia: 18
Fecha f_nac, hoy ; mes: 10 mes: 10
hoy.dia = 18 ; anyo: 2001 anyo: 2001
hoy.mes = 10 ;
hoy.anyo = 2001 ; f_nac hoy
f_nac = hoy ;
}
Asignación.
Es posible utilizar el operador de asignación 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, po-
dríamos 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 más 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, podríamos 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;
Paso como parámetro a subprogramas. Al igual que ocurre con los valores de otros tipos
compuestos, podremos pasar registros como parámetros a subprogramas. Si el parámetro
es de salida, o de entrada/salida, usaremos paso por referencia, y si es de entrada usaremos
paso por referencia constante. Por ejemplo, si quisiéramos leer y escribir valores de tipo Fecha
podríamos declarar los subprogramas:
Registros como valor de retorno de funciones. Aunque está permitida la devolución de regis-
tros como valor de retorno de funciones, es una operación que está desaconsejada, debido a
su alto coste. Por ello, cuando un subprograma necesite devolver un registro, usualmente se
devolverá como un parámetro de salida.
No hay ninguna otra operación disponible con registros completos. Sin embargo, esto no consti-
tuye ninguna limitación, porque el programador puede definir subprogramas que reciban registros
como parámetros. De esta forma puede disponer de operaciones que hagan más simple el diseño
del programa4 .
Podríamos abordar este programa sin necesidad de definir tipos registros, utilizando múltiples
variables para representar cada uno de los elementos manipulados. Por ejemplo, podríamos definir
las variables d1, h1 y m1 para representar la hora, minuto y segundo del primer instante. Proce-
deríamos igual con el segundo, con la diferencia, etc. El resultado sería un programa con multitud
de variables con las que hacer referencia a conceptos que, en realidad, están relacionados. Por este
motivo, resulta más adecuado definir 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
información, pero de una forma más organizada, legible y compacta.
El siguiente programa muestra esta segunda solución. Como se puede ver, la definición 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 descomposición modular del programa, definiendo 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) ;
}
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 ;
}
Posteriormente podremos usar dicho tipo Vector para definir variables y constantes como es
usual. Sin embargo, como ahora tratamos con valores compuestos, las constantes literales del tipo
array se especifican entre llaves dobles. Por ejemplo, a continuación definimos una constante PRIMOS
con los primeros números primos, y una variable v, cuyo valor inicial está sin especificar.
// -- Constantes -------
const Vector PRIMOS = {{ 2, 3, 5, 7, 11 }} ; PRIMOS: 2 3 5 7 11
// -- Principal -------- 0 1 2 3 4
int main ()
{
Vector v; v: ? ? ? ? ?
} 0 1 2 3 4
5 El tipo array de la biblioteca estándar está disponible desde el estándar C++11. Existen otras formas de trabajar
El tipo base (de los elementos) del array puede ser simple o compuesto. Por ejemplo, podemos
definir un nuevo tipo Citas como un agregado de 4 elementos, cada uno del tipo Fecha, y definir
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 }, CUMPLEANYOS:
{ 2, 2, 2002 }, 1 2 3 4
{ 3, 3, 2003 }, 1 2 3 4
{ 4, 4, 2004 } 2001 2002 2003 2004
}} ;
0 1 2 3
int main()
{
Citas cit;
}
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 indivi-
duales. Para ello, en este caso se utiliza el operador ([]), indicando dentro de los corchetes el índice
de la posición que ocupa el elemento al que nos referimos. Por ejemplo, con cit[0] accedemos a
un componente de tipo Fecha situado en la primera posición 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 día almacenado en la fecha situada en el primera
componente del array sea el 22, bastaría 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 válidas 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 número de elementos con que se
ha definido menos uno. Dicho número de elementos es conocido por ser el valor de la constante
utilizada en el typedef (en nuestro ejemplo N_CITAS) , aunque resulta más adecuado utilizarlo
accediendo a la función size() sobre la variable de tipo array correspondiente (en nuestro ejemplo,
cit.size()).
Si por error se intentara acceder a una posición no válida de un array, se estaría generando una
situación anómala. No se produciría ningún aviso de dicho error y, a partir de ese momento, el
programa podría tener un comportamiento inesperado. Por ejemplo, el siguiente programa define
una variable de tipo Citas en la que almacena unas determinadas fechas. Sin embargo, al salir del
bucle accede a una posición errónea.
struct Fecha {
unsigned dia, mes, anyo;
};
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
// ...
}
...
v1 = v2;
Comparación de igualdad (==). Se obtiene true o false según coincida o no cada elemento
del primer array con el elemento correspondiente (en la misma posición) del segundo array.
Es aplicable si el operador == está definido para elementos del tipo base.
Comparación de desigualdad (!=). Se obtiene true si algún elemento del primer array no
coincide con el elemento correspondiente (en la misma posición) del segundo. Es aplicable si
el operador != está definido para elementos del tipo base.
Comparaciones lexicográficas (>, <, >=, <=). Se obtiene true si el primer operando satisface la
operación especificada respecto al segundo. Es aplicable si el operador relacional está definido
para elementos del tipo base.
Paso como parámetro a subprogramas. Al igual que ocurre con los valores de otros tipos
compuestos, podremos pasar arrays como parámetros a subprogramas. Si el parámetro es de
salida, o de entrada/salida, usaremos paso por referencia, y si es de entrada usaremos paso
por referencia constante. Por ejemplo, si quisiéramos leer y escribir valores de tipo Citas
podríamos declarar los subprogramas:
void leer_citas(Citas& c); // parametro de salida
void escribir_citas(const Citas& c); // parametro de entrada
Arrays como valor de retorno de funciones. Aunque está permitida la devolución de arrays
como valor de retorno de funciones, es una operación que está desaconsejada, debido a su alto
coste. Por ello, cuando un subprograma necesite devolver un array, usualmente se devolverá
como un parámetro de salida.
Entrada/salida. Al igual que ocurre en el caso de los tipos registro y, en general, para cualquier
tipo de datos definido por el usuario, no es posible disponer de operaciones predefinidas 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 según el tipo de que se trate.
6 Si el alumno desea utilizar esta opción, debe descargar la biblioteca de la página web de la asignatura
6.4.2. Ejemplos
A continuación, mostraremos la utilidad práctica de la definición de tipos arrays, mediante
ejemplos cuya solución sería poco factible si únicamente dispusiéramos de los tipos vistos hasta
ahora.
Ejemplo 1. Vectores
Supongamos que queremos diseñar un programa que trabaje con vectores de 5 elementos. Sabe-
mos que utilizaremos algunas operaciones típicas sobre los vectores como leer todas las componentes
desde teclado, imprimir el vector, calcular el producto escalar de dos vectores, calcular su suma,
etc. Podríamos haber pensado en declarar 5 variables individuales para representar las componen-
tes de cada vector, (por ejemplo, v11, v12, v13, v14 y v15), pero resulta evidente que este enfoque
nos conduce a una solución inaceptable. Nos llevaría a un programa con multitud de variables
y casi imposible de manejar ¿Qué hacemos si el vector tiene 100 elementos?, ¿cómo abordamos
posibles cambios futuros?. En este ejemplo la única solución factible es definir 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 continuación definimos el tipo Vector y procesamos sus elementos mediante bucles. Ello
nos permite recorrer los elementos, visitando uno a uno cada elemento, para efectuar la operación
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 posición
del elemento del array en la que almacenar el valor leído.
#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);
if (sumar(v1) == sumar(v2)) {
cout << "Misma suma" << endl;
}
}
// -- Principal --------
int main ()
{
Ventas ventas;
leer_ventas(ventas);
imprimir_sueldos(ventas);
}
Definir un elemento reconocible del array que nos permita localizar el punto de frontera entre
ambas zonas.
Contabilizar el número de elementos válidos almacenados en el array.
La primera opción suele requerir la localización del elemento que delimita la frontera entre los
elementos válidos y los no utilizados. Por este motivo, esta opción suele ser, en la mayoría de los
casos, más compleja e ineficiente. Nosotros optaremos por seguir la segunda alternativa. En este
caso, deberemos plantearnos cómo conocer el número de elementos válidos del array. De nuevo,
ahora se plantean dos opciones: mantener dicho número independientemente del array (en una
variable adicional), o bien asociarlo al array al que se refiere, definiendo un registro que contenga
dos campos: el array con los elementos almacenados y el número de elementos válidos del mismo.
En general, optaremos por esta segunda posibilidad, ya que ello da lugar a programas con
mejores características y no introduce complejidad adicional. Este enfoque únicamente requiere
efectuar la correspondiente definición de tipos en base a un registro y el acceso a los elementos del
array y al número 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 gestión de sueldos de los
agentes de ventas, podríamos considerar un número variable de agentes (con un máximo de 20).
De esta forma, ahora definiríamos 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;
};
// -- 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 versión anterior bastaba con leer 20 números, porque sabíamos que siempre había 20 agentes.
Ahora el número de elementos a introducir puede ser diferente y deberemos decidir cómo queremos
que tenga lugar la lectura de datos. En el programa mostramos dos de las opciones más frecuentes
para este tipo de casos:
Que el usuario introduzca datos hasta teclear un valor que indique el fin del proceso de
lectura. En nuestro caso, (en el subprograma leer_ventas_1) detectamos el fin del proceso
de lectura cuando, o bien se han introducido las ventas del número máximo 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 número de datos de agentes a leer en total. Esta
opción resulta más simple, porque se puede programar con el mismo esquema usado para
un número fijo de agentes. Como se puede observar en el subprograma leer_ventas_2, la
única diferencia con el programa para un número fijo de agentes es que ahora el número de
elementos a leer no viene dado por un valor fijo, sino por el valor leído al principio de la
secuencia de entrada. Nótese cómo, para asegurar que el programa no intenta trabajar con
más datos de los previstos, al leer el número de agentes comprobamos que no sea erróneo,
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);
}
En el siguiente ejemplo se define el tipo Matriz, que representa arrays de dos dimensiones cuyos
elementos básicos son de tipo int. Para ello, definimos el tipo Fila como un array unidimensional
de int, y el tipo Matriz como un array de filas.
#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 () m:
{ 0 00 01 02 03 04
Matriz m; 1 10 11 12 13 14
for (unsigned f = 0; f < m.size(); ++f) { 2 20 21 22 23 24
for (unsigned c = 0; c < m[f].size(); ++c) { 0 1 2 3 4
m[f][c] = (f * 10) + c;
}
}
}
Una vez definida una variable m de tipo Matriz, su procesamiento puede requerir trabajar con
una fila completa, en cuyo caso utilizaríamos un único índice. Por ejemplo m[0] hace referencia
a la componente de índice 0 de la matriz m que, según la definición, 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 fila 0,
que es de tipo int.
Del mismo modo, m.size() representa el número de filas de la matriz m, y m[f].size() repre-
senta el número de elementos de la fila f de la matriz m.
#include <iostream>
using namespace std;
// -- Constantes -------
const unsigned NFILAS = 3;
const unsigned NCOLUMNAS = 5;
// -- Tipos ------------
La función sumar_fila recibe como parámetro una fila de la matriz y calcula la suma de sus
elementos. Aunque el programa principal manipula una matriz bidimensional, el valor utilizado
como parámetro actual en la llamada (m[f]) es un elemento de la matriz que, al ser de tipo Fila,
encaja en la definición del parámetro formal correspondiente. Como se puede apreciar, dentro del
subprograma trabajamos con fil que es de tipo Fila, por lo que para acceder a los números a
sumar utilizamos un único índice. En realidad, sumar_fila procesa un array de una dimensión,
independientemente de que la llamada sea una fila 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 función sumar_columna no puede recibir como parámetro una única columna. El programa
no contiene la definición de ningún 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á definido en
el programa. Por tanto, la definición de sumar_columna necesita toda la información necesaria
para acceder a los elementos de una determinada columna. Es decir, la matriz completa y un
número 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 números
a sumar utilizamos dos índices.
Al igual que ocurre con sumar_fila, para escribir una fila de la matriz podemos aprovechar
que existe un tipo Fila, por lo que este subprograma no es más que la escritura en pantalla de un
array de una dimensión.
Además, como hemos definido numerosos subprogramas de apoyo, vemos que el procesamiento
del array bidimensional completo queda reducido a un recorrido típico de un array, accediendo a
una fila completa (m[f]) cada vez que queremos procesar una fila para escribirla en pantalla o para
calcular su suma.
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;
}
Finalmente, aunque también podríamos haber definido un subprograma leer_fila para leer
una fila, e implementar la operación leer_matriz, haciendo llamadas a dicha operación, a conti-
nuación mostramos otro posible enfoque. Ahora implementamos la lectura de elementos fila a fila
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 número leído.
registro con dos campos: uno de ellos contiene el número actual de contactos almacenados y otro
el array con la información detallada de los mismos.
De cada contacto almacenamos su nombre, su dirección y su teléfono, por lo que conviene
agrupar todos estos elementos en un tipo común. Para ello, definimos el tipo Persona como un
registro con tres campos: su nombre y su teléfono, de tipo string, y su dirección. Aunque se podría
haber optado por definir el teléfono como un campo de tipo unsigned, hemos preferido hacerlo de
tipo string porque no pensamos manipularlo con operaciones aritméticas sino con operaciones de
cadenas (por ejemplo, podríamos pensar en obtener la subcadena que determina el prefijo de la
provincia). La dirección de una persona vendrá dada por una calle, un piso, un código postal y una
ciudad. Por ese motivo, definiremos un nuevo tipo registro, llamado Direccion, que lo represente.
Nótese 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 definido el tipo enumerado Cod_Error para definir valores que representen las posibles
situaciones de error en el programa. Como veremos más adelante, usaremos valores dicho tipo para
determinar si una operación se ha realizado correctamente (OK) o por el contrario se ha producido
alguna situación de error al ejecutar el programa.
enum Cod_Error {
OK, AG_LLENA, NO_ENCONTRADO, YA_EXISTE
} ;
Hemos definido una serie de subprogramas que nos permiten descomponer el programa en
operaciones independientes. Utilizamos el subprograma inicializar para obtener una agenda
que esté vacía (es decir, que no contenga ningún elemento). Dado que hemos seguido el criterio
de organizar los elementos de la agenda situándolos contiguos y al principio, y de usar un campo
con el número total de elementos, nos bastará con hacer que el campo n_pers tome el valor cero.
Nótese que, aunque parezca que esta operación tiene poca entidad como para separarla en un
subprograma independiente, en realidad ocurre justamente lo contrario. Se trata de una operación
típica a realizar con la agenda, que debe ser tratada de forma independiente. Ello permite que, por
Al tratar con colecciones de datos surge frecuentemente la necesidad de localizar un dato deter-
minado para procesarlo de alguna forma. Nosotros tratamos las colecciones de datos como parte
de agregados o arrays, por lo que una operación para buscar en qué posición del array se encuentra
un determinado elemento, resulta especialmente útil.
Utilizamos un función buscar_persona que, dada una agenda y el nombre de una persona, nos
indica en qué posición se encuentra, o bien que no se encuentra. Para ello, supondremos que si el
valor devuelto corresponde a una posición del array con un dato válido ello indica que se encuentra
en dicha posición, mientras que si corresponde a una posición no válida 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 ;
}
char menu ()
{
char opcion ;
cout << endl ;
cout << "a. - Añadir 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 Opción: " ;
cin >> opcion ;
} while ( ! (((opcion >= ’a’) && (opcion <= ’e’)) || (opcion == ’x’))) ;
return opcion ;
}
void escribir_cod_error (Cod_Error cod)
{
switch (cod) {
case OK:
cout << "Operación 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 ;
}
}
// -- 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’ ) ;
}
Búsqueda y Ordenación
Al desarrollar programas es frecuente que nos encontremos con situaciones en las que es ne-
cesario acceder a (buscar ) un determinado elemento de una colección con un objetivo concreto.
Podemos estar interesados, por ejemplo, en mostrarlo en pantalla, en eliminarlo o en modificarlo
según resulte conveniente. En el capítulo anterior ya nos percatamos de ello al desarrollar nuestro
programa para gestionar una agenda de contactos. La gran importancia de este tipo de opera-
ciones de búsqueda hace que nos planteemos su estudio de forma más detallada. A continuación
mostraremos algunas posibilidades para abordar la búsqueda. Hay otras posibilidades, como el uso
de técnicas de búsqueda en tablas hash que, aunque son de gran utilidad, no trateremos en este
capítulo.
Hasta ahora hemos tratado con colecciones de elementos almacenadas en arrays, pero no nos
hemos preocupado de la disposición 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, podríamos estar interesados en que los elementos se almacenen
en la agenda ordenados ascendentemente por el nombre de la persona. En este capítulo revisaremos
algunas estrategias típicas para ordenar los elementos en un array.
A continuación, asumiremos que la colección de elementos con la que trabajamos (tanto para
búsqueda como para ordenación) se encuentra almacenada en un array, para lo que usaremos el
siguiente tipo Vector.
//--------------------------------
const unsigned MAXIMO = 50;
typedef array<int, MAXIMO> Vector ;
//--------------------------------
// busca la posición del primer elemento igual a x
// si no se encuentra, retorna v.size()
//-------------
unsigned buscar(int x, const Vector& v);
85
86 CAPÍTULO 7. BÚSQUEDA Y ORDENACIÓN
válida es el índice de un elemento del array mayor que el último elemento de la colección (usaremos
v.size()).
//--------------------------------
// busca la posición 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 afirmativo o negativo. Respondemos en sentido negativo (el elemento no se encuentra) si
el índice del siguiente elemento a probar está más 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 posición i. Nótese que si el elemento no se encuentra se
devuelve v.size(), que es una posición no válida del array.
Un programa que haga uso de la función 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 definidos para el mismo. Por este motivo, es importante que el orden en el que se evalúan las
diferentes condiciones en la expresión de control del fin del bucle sea el mostrado en el algoritmo.
De esa forma, aprovechamos la evaluación en cortocircuito y garantizamos que el bucle se detiene
cuando no hay más elementos a inspeccionar (i >= v.size()), evitando así accesos erróneos a
posiciones no válidas del array. Si hubiéramos escrito el bucle permutando las dos partes de la
expresión de control del bucle:
cc = c;
}
}
}
}
//--------------------------------
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 válido del array y
el índice f (fin) indica el primer elemento no válido. 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
válido). Mientras queden elementos entre los que buscar (i < f), seleccionamos uno con el que
probar. Lo más óptimo es seleccionar el central ((i + f) / 2), porque de esa forma descartamos
//--------------------------------
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 final, y sitúa el menor elemento al principio. Si es necesario, el elemento que ocupaba la
primera posición es situado en la posición que ocupaba el menor. Necesitamos localizar la posición
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]) ;
}
}
posición adecuada. Conseguimos que la zona ordenada tenga un elemento más y la desordenada un
elemento menos. El proceso continúa hasta que la zona desordenada no contiene ningún elemento.
En nuestra implementación usamos pos para delimitar el comienzo de la zona de elementos
desordenados. Todos los elementos a su izquierda estarán ordenados y en cada paso se insertará
el elementos indicado por pos en la zona ordenada del array. Nótese cómo hacemos que pos tome
inicialmente el valor 1. Ello representa la situación inicial, en la que la zona ordenada contiene un
único elemento y la desordenada el resto.
En cada iteración localizamos la posición en la que debemos situar el elemento tratado (v[pos])
y procedemos a su inserción. Si al elemento a insertar le corresponde ir al final de la zona ordenada,
ya se encuentra en su posición correcta, por lo que no habría que hacer nada. Sin embargo, en
cualquier otro caso deberemos garantizar que, una vez situado el elemento en la posición adecuada,
todos los elementos sigan estando ordenados. Para ello, antes de almacenar el elemento en la
posición destino abrimos un hueco en dicha posición, desplazando cada elemento una posición 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 ;
}
}
}
//--------------------------------
//--------------------------------
unsigned buscar_posicion(const Vector& v, unsigned pos) {
unsigned i = 0 ;
while (/*(i < pos)&&*/ (v[pos] > v[i])) {
++i ;
}
return i ;
}
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] ;
}
}
#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 ----
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 posición 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 ;
}
La implementación de buscar_persona pretende ser eficiente. Por ese motivo, evitamos repetir
innecesariamente la comparación de la cadena a buscar y la cadena almacenada en la posición
estudiada en cada paso. En su lugar, hacemos una única comparación y almacenamos su resultado
en la variable cmp1 . Posteriormente, en cada paso decidimos qué hacer en función del contenido de
cmp.
Nótese que en este ejemplo hemos utilizado un algoritmo de búsqueda binaria diferente del
presentado al principio del capítulo, consiguiendo así limitar el número 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 ;
}
1 Usamos la función compare de la biblioteca string, que devuelve 0 si las cadenas comparadas son iguales, un
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 ;
} else {
ok = OK ;
eliminar_ord(ag, i) ;
}
}
//---------------------------
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)
{
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’ ) ;
}
cmath
La biblioteca <cmath> proporciona principalmente algunas funciones matemáticas útiles:
#include <cmath>
using namespace std ;
cctype
La biblioteca <cctype> proporciona principalmente características sobre los valores de tipo
char:
#include <cctype>
using namespace std ;
97
98 CAPÍTULO 8. ALGUNAS BIBLIOTECAS ÚTILES
ctime
La biblioteca <ctime> proporciona principalmente algunas funciones generales relacionadas con
el tiempo:
#include <ctime>
using namespace std ;
#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 ;
#include <cstdlib>
#include <ctime>
using namespace std ;
// -------------------------------------
// inicializa el generador de números aleatorios
inline void ini_aleatorio()
{
srand(time(0)) ;
}
// -------------------------------------
// Devuelve un número aleatorio entre 0 y max (exclusive)
inline int aleatorio(int max)
{
return int(max*double(rand())/(RAND_MAX+1.0)) ;
}
// -------------------------------------
// Devuelve un número aleatorio entre min y max (ambos inclusive)
inline int aleatorio(int min, int max)
{
return min + aleatorio(max-min+1) ;
}
// -------------------------------------
Programación Intermedia
101
Capítulo 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) rápido a la información almace-
nada. Sin embargo, este tipo de memoria es volátil, en el sentido de que los datos almacenados en
ella desaparecen cuando termina la ejecución del programa o se apaga el ordenador. Por este moti-
vo, para almacenar información de manera permanente se utilizan dispositivos de almacenamiento
de memoria secundaria, tales como dispositivos magnéticos (discos duros, cintas), discos ópticos
(CDROM, DVD), memorias permanentes de estado sólido (memorias flash USB), etc.
Los dispositivos de memoria secundaria suelen disponer de gran capacidad de almacenamien-
to, por lo que es necesario alguna organización que permita gestionar y acceder a la información
allí almacenada. A esta organización se la denomina el sistema de ficheros, y suele estar organi-
zado jerárquicamente en directorios (a veces denominados también carpetas) y ficheros (a veces
denominados también archivos). Los directorios permiten organizar jerárquicamente y acceder a
los ficheros, y estos últimos almacenan de forma permanente la información, que puede ser tanto
programas (software) como datos que serán utilizados por los programas.
raiz
Un determinado fichero se puede especificar indicando el camino (o ruta) a seguir para llegar
hasta él dentro del sistema de ficheros. Existen dos mecanismos para especificar este camino: el
absoluto y el relativo.
En el camino absoluto, se especifica la secuencia de directorios por los que se debe pasar para
llegar desde la raíz del sistema de ficheros (indicada por una barra / inicial) hasta el fichero
determinado. Por ejemplo, para el fichero agenda.txt, podemos seguir el siguiente camino
absoluto: /src/agenda.txt
En el camino relativo, se parte desde una determinada posición en el sistema de ficheros,
usualmente el directorio de trabajo, y se especifica la secuencia de directorios por los que
se debe pasar para llegar desde la posición actual en el sistema de ficheros hasta el fichero
determinado (nótese que el direccionamiento relativo no comienza por la barra / inicial),
considerando además que el símbolo .. representa al directorio padre del actual. Por ejem-
plo, si nos encontramos en el directorio de trabajo /bin/, podemos especificar el camino
103
104 CAPÍTULO 9. ALMACENAMIENTO EN MEMORIA SECUNDARIA: FICHEROS
Tipos de Ficheros
Los ficheros se pueden clasificar atendiendo a diferentes criterios. En nuestro caso, nos centrare-
mos en su clasificación en función de la codificación o formato en el que almacenan la información.
Así, podemos distinguir entre ficheros de texto y ficheros binarios.
En los ficheros de texto la información se almacena como una secuencia de caracteres y cada
carácter se almacena utilizando una codificación estándar (usualmente basada en la codificación
ASCII, UTF-8, etc). Al tratarse de un formato estandarizado, otros programas diferentes de aquel
que creó el fichero podrán entender y procesar su contenido. Por ejemplo, un programa podría
generar un fichero de texto con los datos de las personas de una agenda y posteriormente dicho
fichero podría ser entendido y procesado por otros programas. Por ejemplo, podría ser visualizado
y editado mediante programas de edición de textos de propósito general, tales como gedit, kate,
gvim, emacs, etc. en Linux, textedit en MacOS-X y notepad en Windows, entre otros.
En los ficheros binarios, la información se almacena con el mismo formato y codificación uti-
lizada para su almacenamiento en memoria principal. Están concebidos para ser procesados au-
tomáticamente por programas que conocen su formato interno. Un programa no podrá procesar
la información que contiene si no dispone de documentación adecuada que describa su formato
interno1 . El procesamiento de ficheros binarios es más eficiente que el de ficheros de texto porque
la información está representada directamente en código binario (exactamente tal y como se en-
cuentra internamente en la memoria principal). De esta forma se evita la pérdida de tiempo que
ocasionaría su conversión a un formato estándar (ASCII, UTF-8, etc), como ocurre en los ficheros
de texto.
En el caso del software, los programas en código fuente codificados en un lenguaje de progra-
mación suelen ser almacenados como ficheros de texto. Sin embargo, el resultado de compilar estos
programas fuente a programas ejecutables se almacenan en ficheros binarios (ejecutables por el
Sistema Operativo). Así mismo, los ficheros que contienen imágenes, vídeo y música suelen estar,
en su mayoría, almacenados en formato binario.
Consideremos un ejemplo concreto y supongamos que disponemos de un fichero de texto de-
nominado fechas.txt, que podría estar almacenado en una determinada posición en la jerarquía
del sistema de ficheros (/home/alumno/documentos/fechas.txt) y contener información sobre
las fechas de nacimiento de determinadas personas según el siguiente formato, donde cada línea se
encuentra terminada por un carácter terminador de fin de línea:2
Aunque los datos almacenados en memoria se encuentran en formato binario, son convertidos a
su representación textual antes de ser escritos en el fichero. Similarmente, cuando leemos del fichero
de texto para almacenar la información en memoria se produce una conversión de formato de texto
a fomato binario. Por ejemplo, si el número 12 se almacena en una variable de tipo unsigned su
representación interna utilizaría un número binario de 32 bits (00000000000000000000000000001100).
Sin embargo, su representación textual en el fichero 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 fichero de texto se almacenan los dos
caracteres con el valor que corresponda según el código ASCII (2 bytes).
1 Si un fichero binario es procesado por un programa que se ejecuta en un ordenador que utiliza una representación
interna distinta de la utilizada en el ordenador en que se creó podemos tener problemas de compatibilidad.
2 El carácter terminador de fin de línea no es visible, aunque se aprecian sus efectos al mostrarse los siguientes
Por el contrario, un flujo de salida de datos en modo texto actúa como un sumidero que recibe una
secuencia de caracteres (usualmente a través de un buffer de almacenamiento intermedio) al que se
envían los caracteres que representan a los datos de salida, que previamente han sido convertidos
al formato de texto adecuado.
Pepe 111
Pepe 444
Maria 222
Maria 666
Juan 333 Datos (f_entrada) (f_salida) Datos Juan 555
agenda.txt
agenda.txt
7. Comprobar que el procesamiento del fichero 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 final del fichero. 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 flujo para liberar la variable manejador de su vinculación con el fichero. En
caso de no cerrar el flujo, éste será cerrado automáticamente cuando termine el ámbito de vida de
la variable manejador del fichero.
Nota: es importante tener en cuenta que cuando un flujo pasa al estado erróneo (fail()),
entonces cualquier operación de entrada o salida que se realice sobre él también fallará.
Las variables de tipo flujo pueden ser usadas como parámetros de subprogramas. En este caso
hay que tener en cuenta que es necesario que, tanto si el fichero se quiere pasar como un parámetro
de entrada, salida o entrada/salida, dicho paso de parámetro debe hacerse por referencia (no
constante).
#include <fstream>
using namespace std;
2. Necesitamos definir una variable que actúe como manejador del fichero del que queremos leer. Esta
variable debe ser de tipo ifstream (input file 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 flujo, pero aún no hemos
procedido a realizar una vinculación con un fichero concreto. Al proceso de vincular una variable
manejador de fichero con un determinado fichero se le conoce como abrir el fichero.
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 fichero fechas.txt. En numerosas ocasiones el nombre del fichero a abrir no
es siempre el mismo, podemos tener programas en los que, por ejemplo, el nombre del fichero 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 función c_str()
para adaptar la variable al tipo correcto antes de efectuar la llamada a open, como se muestra a
continuación.
string nom_fich;
4. Comprobar que la apertura del fichero se realizó correctamente y evitar el procesamiento del fichero
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 información leída. Por ejemplo, para un fichero con el formato mostrado en la sección 9, podríamos
leer dos cadenas con el nombre y apellidos de la persona (hasta el primer separador) y tres números
con el día, mes y año de nacimiento de la persona3 .
3 Suponemos que cada uno de los datos leídos se encuentran seguidos de un separador en el fichero.
f_ent >> nombre >> apellidos >> dia >> mes >> anyo;
o bien podríamos estar interesados en leer una línea completa en una variable de tipo string.
string linea;
getline(f_ent, linea);
En el capítulo 6.2.1 estudiamos que la lectura de datos puede requerir limpiar el buffer de entrada
en determinada situaciones. Ahora, aunque el flujo desde el que efectuar la entrada es un fichero (en
lugar de cin) el comportamiento es exactamente el mismo que estudiamos en dicho capítulo. Por
ese motivo, si utilizamos getline para leer una cadena del fichero, es posible que necesitemos omitir
(limpiar ) determinados caracteres del buffer de entrada del fichero para asegurarnos de que la lectura
se realiza correctamente. En tal caso, utilizaríamos el manipulador ws o bien la función ignore (sobre
la variable manejador del fichero) para limpiar el buffer de entrada, al igual que hicimos en dicho
capítulo.
f_ent >> edad ; f_ent >> edad ;
f_ent.ignore(1000, ’\n’) ; //---------------------------
//--------------------------- f_ent >> ws ;
getline(f_ent, linea) ; getline(f_ent, linea) ;
Al igual que podemos usae la función get para leer un carácter de teclado, podemos utilizar dicha
función para leer un carácter de un fichero. En tal caso deberemos indicar el manejador del fichero
del que queremos realizar la lectura.
char car;
f_ent.get(car);
Usualmente la lectura de datos de un fichero se realiza dentro de un proceso iterativo que acaba
cuando el fichero no contiene datos que procesar. Necesitamos algún modo de detectar que se ha
alcanzado el fin de un fichero durante un proceso de lectura. Para ello, comprobaremos si se ha
producido un fallo al intentar efectuar una operación 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 leídos, 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 fichero se realizó correctamente, es decir, que el fichero se
leyó completamente hasta el final de mismo (eof representa end-of-file). Si el fichero no acabó
correctamente es porque se produjo un error de lectura durante su procesamiento. En tal caso,
posiblemente nos interesará tratar adecuadamente dicha situación de error.
if (!f_ent.fail() || f_ent.eof()) { /* OK */ }
7. Finalmente, cerrar el flujo liberando la variable de su vinculación.
f_ent.close();
Acabaremos esta sección mostrando un ejemplo completo en el que utilizamos todos los elemen-
tos introducidos anteriormente. Queremos leer números4 de un fichero y mostrarlos en pantalla. En
numerosas ocasiones el esquema del programa mostrado a continuación puede ser utilizado en otros
programas. Para ello bastaría con modificar 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 leídos.
4 Se supone que los números están separados por espacios o separadores adecuados.
#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)
{
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);
}
#include <fstream>
using namespace std;
2. Necesitamos definir una variable que actúe como manejador del fichero al que queremos escribir. Esta
variable debe ser de tipo ofstream (output file 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 flujo, pero aún no hemos
procedido a realizar una vinculación con un fichero concreto. Al proceso de vincular una variable
manejador de fichero con un determinado fichero se le conoce como abrir el fichero.
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 fichero fechas.txt. En numerosas ocasiones el nombre del fichero a abrir no
es siempre el mismo, podemos tener programas en los que, por ejemplo, el nombre del fichero 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 función c_str()
para adaptar la variable al tipo correcto antes de efectuar la llamada a open, como se muestra a
continuación.
string nom_fich;
4. Comprobar que la apertura del fichero se realizó correctamente y evitar el procesamiento del fichero
en caso de fallo en la apertura.
if (f_sal.fail()) { ... }
5. Si escribimos datos en un fichero es con la intención 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 fichero como el usado en la sección anterior, será necesario escribir los separadores entre cada
dos valores. Por ejemplo, a continuación utilizamos espacios en blanco entre cada dos datos de una
persona y un salto de línea 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 finaliza cuando se
escriben en el fichero todos los datos apropiados y mientras el estado del flujo sea correcto.
if (!f_sal.fail()) { /* OK */ }
f_sal.close();
Por ejemplo, a continuación se muestra un programa que lee números de teclado (hasta in-
troducir un cero) y los escribe en un fichero de texto. Los números se escriben separados por un
carácter de fin de línea (endl), lo que permite que el fichero aparezca con cada número en una
línea diferente si es mostrado por un editor o que los números puedan ser leídos y procesados por
un programa como el presentado en el ejemplo anterior.
#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. Esta-
mos acostumbrados a resolver problemas como éste, en los que trabajamos con secuencias de núme-
ros que se leen de teclado hasta que se cumple una cierta condición. En este caso hemos incluido dos
condiciones adicionales en la expresión que controla el fin del bucle (! cin.fail() && ! f_sal.fail()).
Con la primera condición (! cin.fail()) nos aseguramos de que el proceso iterativo se detiene
si se produce algún error durante la lectura de teclado y el flujo 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 suposición puede resultar peligrosa en
un programa como el mostrado en este ejemplo porque, en caso de ocurrir alguún error de lectura
(por ejemplo, si en lugar de introducir un número de tipo int el usuario introduce una cadena de
caracteres), el flujo de entrada cin entraría en un estado de error, dando lugar a un bucle infinito
que podría hacer que el fichero creciera sin control hasta ocupar todo el espacio disponible en el
dispositivo que almacena el fichero. La inclusión de esta condición evita este riesgo.
Con la segunda condición (! f_sal.fail()) nos aseguramos de que el proceso iterativo se
detiene si se produce algún error durante la escritura en el fichero, por ejemplo, si el dispositivo no
dispone de memoria suficiente.
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;
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);
}
9.4. Ejemplos
Ejemplo 1. Copia del contenido de un fichero en otro.
A continuación, mostramos un programa que lee carácter a carácter el contenido de un fichero
de texto y crea un nuevo fichero con el mismo contenido. El programa se basa en el subprograma
copiar_fichero, que recibe dos cadenas de caracteres con el nombre de los ficheros origen y
destino, realiza la copia y devuelve el estado de error resultante de efectuar la operación.
#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();
}
}
En este ejemplo trabajamos con dos ficheros simultáneamente, 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 fichero hay que asociar su nombre con el manejador correspondiente,
posteriormente se efectúan las operaciones que corresponda con cada uno y, finalmente, se liberan
las vinculaciones de los manejadores con los ficheros5 . En este caso el procesamiento del fichero de
entrada en cada paso consiste en leer un carácter (usamos get porque queremos leer también los
separadores) y el procesamiento del fichero de salida consiste en escribir un único carácter.
Ejemplo 2
Ejemplo de un programa que crea, guarda y carga una agenda personal.
//-------------------------------------------------------------------------
#include <iostream>
#include <fstream>
#include <string>
5 Los manejadores de ficheros son en este caso variables locales, que se destruyen automáticamente al acabar
el subprograma, liberando los recursos necesarios. Así pues, en este caso podríamos haber omitido la liberación
explícita (close). Sin embargo, optamos por incluirla porque ello refleja claramente el proceso a seguir en cualquier
manipulación de ficheros (abrir, usar, cerrar)
#include <array>
#include <cctype>
using namespace std ;
//-------------------------------------------------------------------------
struct Fecha {
unsigned dia ;
unsigned mes ;
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 año): " ;
cin >> f.dia >> f.mes >> f.anyo ;
}
void leer_persona(Persona& p)
{
cout << "Introduza nombre: " ;
cin >> ws ;
getline(cin, p.nombre) ;
cout << "Introduza teléfono: " ;
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’) ;
}
}
//-------------------------------------------------------------------------
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 << "Teléfono: " << 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>
// <teléfono> <dia> <mes> <año> <RC>
// <nombre> <RC>
// <teléfono> <dia> <mes> <año> <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 implementación
// 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 ;
} 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>
// <teléfono> <dia> <mes> <año> <RC>
// <nombre> <RC>
// <teléfono> <dia> <mes> <año> <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 ;
Módulos y Bibliotecas
Permite aumentar la localidad y cohesión del código, aislándolo del exterior. Es decir, permite
separar y aislar el código encargado de resolver un determinado problema.
Facilita la reutilización del código. Es posible, tanto utilizar las bibliotecas del sistema como
crear módulos con nuevas bibliotecas que puedan ser reutilizadas por múltiples programas.
Esta distribución de bibliotecas se puede hacer en código objeto, por lo que no es necesario
distribuir el código fuente de la misma.
M1 Main M2 Programa
119
120 CAPÍTULO 10. MÓDULOS Y BIBLIOTECAS
privada), y otro que contiene las definiciones de tipos, constantes y prototipos de subprogramas
que el módulo ofrece (la parte pública). Hablaremos de la implementación del módulo cuando nos
refiramos al fichero que contiene su parte privada, y usaremos el témino interfaz del módulo para
referirmos al fichero que contiene su parte pública1 .
Normalmente un programa completo se compone de varios módulos, cada uno con su fichero
de encabezamiento (interfaz) y con su fichero de implementación, y de un módulo principal donde
reside la función principal main. Los ficheros de implementación tendrán una extensión “.cpp”
(también suelen utilizarse otras extensiones como “.cxx” y “.cc”) y los ficheros de encabezamiento
tendrán una extensión “.hpp” (también suelen utilizarse otras extensiones como “.hxx”, “.hh” y “.h”).
Cuando en un determinado módulo se desee hacer uso de las utilidades proporcionadas por
otro módulo, éste deberá incluir el fichero de encabezamiento (interfaz) del módulo que se vaya a
utilizar. Además, el fichero de implementación de un determinado módulo deberá incluir al fichero
de encabezamiento de su propio módulo. Por ejemplo, si quisiéramos obtener un programa en
el que se trabaje con números complejos, podríamos pensar en disponer de un módulo que se
encargue de definir el tipo Complejo junto a una serie de operaciones. En el fichero de interfaz
(complejo.hpp) se encontraría la definición del tipo y la declaración de los subprogramas y en el
fichero de implementación (complejo.cpp) se encontraría la implementación de los subprogramas.
El módulo principal debería importar el fichero de interfaz (complejo.hpp) para poder hacer uso
del tipo Complejo y de los subprogramas. Además, el módulo de implementación también debería
importar a su fichero de interfaz para poder conocer aquello que se desea implementar. El siguiente
dibujo muestra el esquema básico de los ficheros usados en este ejemplo, donde se utiliza la directiva
#include para incluir los ficheros de encabezamiento que corresponda:
Las definiciones en los ficheros de encabezamiento (interfaz) serán especificadas entre las guar-
das (directivas de compilación condicional) para evitar la inclusión duplicada de las definiciones
allí contenidas, durante la compilación de un determinado módulo. El nombre de la guarda usual-
mente se deriva del nombre del fichero, como se indica en el siguiente ejemplo donde el módulo
complejo tendrá los siguientes ficheros de encabezamiento y de implementación (en determinadas
circunstancias, puede ser conveniente que al nombre de la guarda se le añada también el nombre
del espacio de nombres que se explicará en la siguiente sección):
1A este fichero también se le denomina “fichero de encabezamiento” o “fichero de cabecera” (header file en inglés).
Para compilar un módulo de forma independiente, únicamente hay que con compilar su fichero
de implementación, por ejemplo complejo.cpp. El resultado es un fichero en código objeto, por
ejemplo complejo.o. El compilador compila un código fuente que será el resultado de incluir en
el fichero de implementación el contenido de los ficheros de encabezamiento, como se indica en
la figura anterior. En cualquier caso, una vez que tenemos los ficheros objetos de los diferentes
módulos, se procede a su enlazado final, obteniendo el fichero ejecutable.
Dependiendo del entorno en el que trabajemos, la tarea de compilación podrá ser realizada, bien
directamente desde la línea de comando o con herramientas adecuadas en un entorno de desarrollo
integrado. A continuación mostramos algunas de las opciones que se pueden realizar, suponiendo
que trabajamos directamente desde la línea de comando y que utilizamos el compilador GNU GCC.
Podríamos generar los ficheros objetos de los dos módulos 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 códigos objeto generados en el punto anterior para generar el fichero ejecutable main,
con el siguiente comando:
g++ -ansi -Wall -Werror -o main main.o complejo.o
Hay otras posibilidades, también es posible realizar la compilación y enlazado en el mismo comando:
g++ -ansi -Wall -Werror -o main main.cpp complejo.cpp
Hay que tener en cuenta que el compilador enlaza automáticamente el código generado con
las bibliotecas estándares de C++ y, por lo tanto, no es necesario que éstas se especifiquen explí-
citamente. Sin embargo, en caso de ser necesario, también es posible especificar el enlazado con
bibliotecas externas:
g++ -ansi -Wall -Werror -o main main.cpp complejo.cpp -ljpeg
Estas bibliotecas no son más que una agregación de módulos compilados a código objeto, y orga-
nizadas adecuadamente para que puedan ser reutilizados por muy diversos programas.
identificador sea utilizado en un módulo o biblioteca para nombrar a una determinada entidad y
en otro módulo diferente se utilice para nombrar a otra entidad diferente. Por ejemplo, podríamos
tener una biblioteca que defina en su interfaz una constante llamada MAX, con valor 100, y otra
biblioteca diferente que utilice el mismo identificador para definir 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 algún mecanismo para identificar
exactamente a qué entidad nos referimos en cada caso.
El lenguaje de programación C++ permite solucionar este tipo de situaciones ambiguas me-
diante el uso de espacios de nombres (namespace en inglés), que permiten agrupar bajo una misma
denominación (jerarquía) un conjunto de declaraciones y definiciones, de tal forma que dicha deno-
minación será necesaria para identificar y diferenciar cada entidad declarada. En nuestro ejemplo,
una de las constantes MAX sería definida en el ámbito de un espacio de nombres concreto y la otra
en el ámbito de otro espacio de nombres diferente. Posteriormente podríamos referirnos a cada
una de ellas indicando el identificador y el espacio de nombres en el que está definida, lo que evita
cualquier posibilidad de ambigüedad.
Para definir un espacio de nombres se utiliza la palabra reservada namespace seguida por el
identificador del espacio de nombres, y entre llaves las declaraciones y definiciones que deban estar
bajo dicha jerarquía del espacio de nombres.
Los espacios de nombres pueden ser únicos para un determinado módulo o, por el contrario,
pueden extenderse a múltiples módulos y bibliotecas gestionados por el mismo proveedor. Por
ejemplo, todas las entidades definidas en la biblioteca estándar se encuentran bajo el espacio de
nombres std.
El identificador del espacio de nombres puede ser derivado del propio nombre del fichero, puede
incluir una denominación relativa al proveedor del módulo, o alguna otra denominación más com-
pleja que garantice que no habrá colisiones en el identificador del espacio de nombres. Por ejemplo,
podemos definir el módulo complejo dentro del espacio de nombres umalcc, que haría referencia a
un proveedor del departamento de Lenguajes y Ciencias de la Computación de la Universidad de
Málaga.
Todos los identificadores definidos dentro de un espacio de nombres determinado son visi-
bles y accesibles directamente desde dentro del mismo espacio de nombres, sin necesidad de
calificación.
nombres, seguido del operador :: y del identificador de la entidad a la que queremos aludir.
Por ejemplo, para aludir a un identificador MAX, definido en el espacio de nombres umalcc,
habría que utilizar umalcc::MAX.
En el caso de ficheros de implementación (con extensión .cpp) podemos estar interesados
en utilizar con frecuencia identificadores de un mismo espacio de nombres. En estos casos
resultaría más cómodo poder omitir la calificación explícita. En estos casos resulta más có-
modo 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 iden-
tificadores de dicho espacio de nombres completo, que podrán ser accedidos directamente,
sin necesidad de calificación explícita. Esto no resulta nuevo para nosotros, ya que estamos
acostumbrados a utilizar dicha directiva para hacer accesible el espacio de nombres std
Por ejemplo, como las bibliotecas estándar (iostream, string o fstream) se definen en el
espacio de nombres std, para usar alguna de sus entidades (por ejemplo, el tipo string),
tenemos dos posibilidades: utilizar una calificación explícita 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 identificadores están definidos en el espacio de nombres
std.
El uso de la directiva using namespace no está recomendado en los ficheros de encabeza-
miento, donde siempre utilizaremos calificación explícita. Por ejemplo, si queremos crear
un módulo con un fichero de encabezamiento personas.hpp, optaríamos por calificar explíci-
tamente los identificadores 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 simultáneamente. En tal caso existe la posibilidad de que volvamos a tener problemas de
colisión entre los identificadores. Puede haber varios identificadores que definen entidades diferentes
y que son accesibles en dos espacios de nombres diferentes. Ello no supone ningún problema si
nuestro programa no utiliza los identificadores en conflicto. Sin embargo, en caso de utilizarlos
el compilador no podría saber a qué entidad nos estamos refiriendo. Para solucionar este tipo de
conflictos el programador debe utilizar la calificación explícita con este identificador, eliminando
así la ambigüedad en su utilización.
otros módulos, por lo que se pondrían disponibles (accesibles) todos los identificadores de dicho
espacio de nombres en todos los ficheros que incluyan (include) dicho fichero de encabezamiento
y ello podría provocar colisiones inesperadas y no deseadas.
//----------------------------------
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 ----------------------------------------------
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
Como las entidades del fichero de encabezamiento fueron definidas en el espacio de nombres
umalcc, ahora procedemos a su implementación en dicho espacio de nombres. Los identificadores
que dan nombre a dichas entidades son directamente accesibles en dicho espacio de nombres y, por
tanto, pueden ser utilizados directamente, sin necesidad de calificación (por ejemplo, Complejo).
namespace umalcc {
//----------------------------------
//-- Implementación ----------------
//----------------------------------
// 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 (igual(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 igual(a.real, b.real) && igual(a.imag, b.imag) ; }
//----------------------------------
Una vez que disponemos del módulo para operar con números complejos, a continuación mos-
tramos un ejemplo de su utilización en el siguiente programa, que contiene algunos subprogramas
para realizar pruebas de las operaciones básicas del mismo.
Nuestro módulo principal comienza incluyendo la biblioteca estándar iostream, porque en el
mismo utilizaremos operaciones de entrada/salida con la consola. Además, declara variables de tipo
Complejo y hace uso de diferentes subprogramas para trabajar con dicho tipo. Como consecuencia,
debemos incluir el fichero de encabezamiento con el interfaz de dicho módulo (complejos.hpp),
consiguiendo así que todas sus definiciones y declaraciones sean visibles en el módulo principal.
Sabemos que las entidades de la biblioteca iostream han sido definidas en el espacio de nombres
std y las del módulo complejos han sido definidas en el espacio de nombres umalcc. Utilizamos
la directiva using namespace para evitar la necesidad de utilizar calificación explícita cuando
hagamos uso de las entidades definidas en las mismas. Sin embargo, hay una situación en la que
nos vemos obligados a utilizar calificación explícita. Como se puede observar, el identificador leer
tiene dos usos diferentes. Por una parte, es el nombre del subprograma utilizado para leer un
número complejo, que ha sido declarado en el interfaz del módulo (complejos.hpp). Por otra parte,
en el módulo principal tenemos otro subprograma que también se llama leer, tiene los mismos
parámetros, pero no tiene ninguna relación con el primero. Si queremos utilizar el subprograma
leer que ha sido definido en el interfaz del módulo, es necesario que utilicemos el identificador
el espacio de nombres (umalcc) para calificar explícitamente el nombre del subprograma, como se
puede ver a continuación. Nótese que, para invocar al subprograma leer del módulo principal, se
debe calificar explícitamente con :: directamente desde el espacio de nombres global.
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 ;
Complejo aux ;
multiplicar(aux, c0, c2) ;
if (! iguales(c1, aux)) {
cout << "Error en operaciones de mult/div"<< endl ;
}
}
//------------------------------------
int main()
{
Complejo c1, c2 ;
// calificación explícita del espacio de nombres global
// para evitar colisión en invocación 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 ----------------------------------------------
A medida que aumenta la complejidad del problema a resolver, del mismo modo deben aumen-
tar los niveles de abstracción necesarios para diseñar y construir su solución algorítmica. Así, la
abstracción procedimental permite aplicar adecuadamente técnicas de diseño descendente y refina-
mientos sucesivos en el desarrollo de algoritmos y programas. La programación modular permite
aplicar la abstracción 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 también la complejidad de las estructuras de datos necesarias para su resolución,
y este hecho requiere, así mismo, la aplicación de la abstracción a las estructuras de datos.
La aplicación de la abstracción a las estructuras de datos da lugar a
los Tipos Abstractos de Datos (TAD), donde se especifica el concepto que
TAD
representa un determinado tipo de datos, y la semántica (el significado) de
op1()
las operaciones que se le pueden aplicar, pero donde su representación e im-
plementación internas permanecen ocultas e inaccesibles desde el exterior, op2()
de tal forma que no son necesarias para su utilización. Así, podemos consi-
op3()
derar que un tipo abstracto de datos encapsula una determinada estructura
abstracta de datos, impidiendo su manipulación directa, permitiendo sola-
mente su manipulación a través de las operaciones especificadas. De este modo, los tipos abstractos
de datos proporcionan un mecanismo adecuado para el diseño y reutilización de software fiable y
robusto.
Para un determinado tipo abstracto de datos, se pueden distinguir tres niveles:
Nivel de implementación, donde se define e implementa tanto las estructuras de datos que
soportan la abstracción, como las operaciones que actúan sobre ella según la semántica es-
pecificada. Este nivel interno permanece privado, y no es accesible desde el exterior del tipo
abstracto de datos.
Nótese que para una determinada especificación de un tipo abstracto de datos, su implementación
puede cambiar sin que ello afecte a la utilización del mismo.
131
132 CAPÍTULO 11. TIPOS ABSTRACTOS DE DATOS
class Complejo {
public:
// ... zona pública ...
private:
// ... zona privada ...
} ;
Atributos
class Complejo {
public:
// ...
private:
double real ; // parte real del numero complejo
double imag ; // parte imaginaria del numero complejo
} ;
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 especifica
ninguna forma explícita de construcción. Así, será el encargado de construir el objeto con los valores
iniciales adecuados en el momento en que sea necesaria dicha construcción, por ejemplo cuando el
flujo de ejecución alcanza la declaración de una variable de dicho tipo (véase 11.1.3).
Los constructores se declaran con el mismo identificador de la clase, seguidamente se especifican
entre paréntesis los parámetros necesarios para la construcción, que en el caso del constructor por
defecto, serán vacíos. Nótese que la definición del constructor no especifica ningún tipo de valor
devuelto (ni siquiera void).
class Complejo {
public:
Complejo() ; // Constructor por Defecto
// ...
private:
// ...
} ;
class Complejo {
public:
// ...
//----------------------------
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:
// ...
} ;
int main()
{ real: 0.0 real: 0.0
Complejo c1, c2 ; imag: 0.0 imag: 0.0
// ...
c1 c2
}
//- fin: main.cpp ---------------------------------------------------
Es importante remarcar que cada objeto, definido de una determinada clase, es una instancia
independiente de los otros objetos definidos de la misma clase, con su propia memoria para contener
de forma independiente el estado de su representación interna.
int main()
{
Complejo c1 ; // construcción de c1 (1 vez)
for (int i = 0 ; i < 3 ; ++i) {
Complejo c2 ; // construcción de c2 (3 veces)
// ...
} // destrucción de c2 (3 veces)
// ...
} // destrucción de c1 (1 vez)
//- fin: main.cpp ---------------------------------------------------
int main()
{
Complejo c1, c2, c3 ; // construcción de c1, c2, c3
c1.leer() ;
real: 5.3 real: 2.5 real: 7.8
c2.leer() ;
c3.sumar(c1, c2) ; imag: 2.4 imag: 7.3 imag: 9.7
c3.escribir() ; c1 c2 c3
} // destrucción de c1, c2, c3
//- fin: main.cpp ---------------------------------------------------
Métodos
En la implementación de un determinado método de una clase, éste método puede invocar
directamente a cualquier otro método de la clase sin necesidad de aplicar el operador punto (.).
Así mismo, un método de la clase puede acceder directamente a los atributos del objeto sobre el que
se invoque dicho método, sin necesidad de aplicar el operador punto (.), ni necesidad de recibirlo
como parámetro. 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 parámetros, si son accesibles
desde la implementación de una determinada clase, es necesario especificar el objeto (mediante su
identificador) seguido por el operador punto (.) y a continuación el identificador del atributo en
cuestión.
Así, podemos ver como para calcular la suma de números complejos, se asigna a las partes
real e imaginaria del número complejo que estamos calculando (el objeto sobre el que se aplica el
método sumar) la suma de las partes real e imaginaria respectivamente de los números complejos
que recibe como parámetros. Por ejemplo, cuando se ejecuta la sentencia:
c3.sumar(c1, c2) ;
almacenará en el atributo real del número complejo c3 el resultado de sumar los valores del
atributo real de los números complejos c1 y c2. De igual modo sucederá con el atributo imag del
número complejo c3, que almacenará el resultado de sumar los valores del atributo imag de los
números complejos c1 y c2.
Constructores
En la implementación de los constructores de la clase, también será calificado explícitamente
con el identificador de la clase correspondiente. Después de la definición de los parámetros, a
continuación del delimitador (:), se especifica la lista de inicialización, donde aparecen, separados
por comas y según el orden de declaración, todos los atributos miembros del objeto, así como
los valores con los que serán inicializados especificados entre paréntesis (se invoca al constructor
adecuado según los parámetros especificados entre paréntesis, de tal forma que los paréntesis vacíos
representan la construcción por defecto). A continuación se especifican entre llaves las sentencias
pertenecientes al cuerpo del constructor para realizar las acciones adicionales necesarias para la
construcción del objeto. Si no es necesario realizar ninguna acción adicional, entonces el cuerpo del
constructor se dejará vacío. Nótese que la definición del constructor no especifica ningún tipo de
valor devuelto (ni siquiera void).
Por ejemplo, implementaremos el constructor por defecto de la clase Complejo para que ini-
cialice cada atributo (parte real e imaginaria) del objeto que se construya invocando a su propio
constructor por defecto respectivamente (según su tipo):
Complejo::Complejo() // Constructor por Defecto
: real(), imag() { }
11.1.5. Ejemplo
Por ejemplo, el TAD número complejo representa el siguiente concepto matemático de número
complejo:
Definición
//- fichero: complejos.hpp ------------------------------------------
#ifndef complejos_hpp_
#define complejos_hpp_
namespace umalcc {
//----------------------------------
const double ERROR_PRECISION = 1e-6 ;
//----------------------------------
class Complejo {
public:
//----------------------------------------------------------
//-- Métodos Públicos --------------------------------------
//----------------------------------------------------------
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 ----------------------------------------------
Implementación
//- 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
//------------------------------------------------------------------------------
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 {
//----------------------------------------------------------
//-- Métodos Públicos --------------------------------------
//----------------------------------------------------------
Complejo::Complejo() // Constructor por Defecto
: real(0.0), imag(0.0) { }
//----------------------------
// devuelve la parte real del numero complejo
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 ----------------------------------------------
Utilización
//- fichero: main.cpp -----------------------------------------------
#include <iostream>
#include "complejos.hpp"
using namespace std ;
using namespace umalcc ;
//------------------------------------
void leer(Complejo& c)
{
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) ;
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 ---------------------------------------------------
class ListaInt {
public:
// ...
private:
static const int MAX = 256 ;
// ...
} ;
Usualmente las constantes se definen en la zona privada de la clase, por lo que usualmente sólo serán
accesibles internamente desde dentro de la clase. Sin embargo, en algunas situaciones puede ser
conveniente definir la constante en la zona pública de la clase, entonces en este caso la constante
podrá ser accedida desde el exterior de la clase, y será utilizada mediante calificación explícita
utilizando el identificador de la clase. Por ejemplo:
class ListaInt {
public:
static const int MAX = 256 ;
// ...
private:
// ...
} ;
// ...
int main()
{
int x = ListaInt::MAX ;
// ...
}
Sin embargo, la definición 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 más compleja, por lo
que usualmente se realizará externamente a la definición de la clase, dentro del ámbito del módulo
(en el fichero hpp, dentro del espacio de nombres del módulo, si debe ser pública, y en el fichero
cpp, dentro del espacio de nombres anónimo, si debe ser privada).
Usualmente los tipos se definen en la zona privada de la clase, por lo que usualmente sólo serán
accesibles internamente desde dentro de la clase. Sin embargo, en algunas situaciones puede ser
conveniente definir el tipo en la zona pública de la clase, entonces en este caso el tipo podrá ser
accedido desde el exterior de la clase, y será utilizado mediante calificación explícita utilizando el
identificador de la clase. Por ejemplo:
#include <array>
// ...
class ListaInt {
public:
static const int MAX = 256 ;
typedef std::array<int, MAX> Datos ;
// ...
private:
// ...
} ;
// ...
int main()
{
ListaInt::Datos d ;
// ...
}
Nótese que los tipos deben ser públicos si forman parte de los parámetros de los métodos públicos,
de tal forma que puedan ser utilizados externamente, allá donde sea necesario invocar a dichos
métodos públicos.
Constructores Específicos
class Complejo {
public:
Complejo(double p_real, double p_imag) ; // Constructor Específico
} ;
A continuación se puede ver como sería la implementación de este constructor específico, donde se
inicializa el valor de cada atributo con el valor de cada parámetro recibido en la invocación de la
construcción del objeto.
Finalmente, a continuación podemos ver un ejemplo de como sería una posible invocación a dicho
constructor específico (para c2), junto a una invocación al constructor por defecto (para c1):
int main()
{ real: 0.0 real: 2.5
Complejo c1 ; imag: 0.0 imag: 7.3
Complejo c2(2.5, 7.3) ;
c1 c2
// ...
}
//- fin: main.cpp ---------------------------------------------------
Como se explicó anteriormente (véase 11.1.2 y 11.1.4), el constructor por defecto es el meca-
nismo por defecto utilizado para construir objetos de este tipo cuando no se especifica ninguna
forma explícita de construcción. Así, será invocado automáticamente cuando se deba construir un
determinado objeto, sin especificar explícitamente el tipo de construcción requerido, en el momen-
to en que sea necesaria dicha construcción, por ejemplo cuando el flujo de ejecución alcanza la
declaración de una variable de dicho tipo (véase 11.1.3).
El constructor por defecto es un método especial de la clase, ya que si el programador no define
ningún constructor para una determinada clase, entonces el compilador generará e implementará
automáticamente dicho constructor con el comportamiento por defecto de invocar automática-
mente al constructor por defecto para cada atributo de tipo compuesto miembro de la clase. Nótese,
sin embargo, que en el caso atributos de tipo simple, la implementación automática del compilador
los dejará sin inicializar.
No obstante, el programador puede definir el constructor por defecto para una determinada
clase cuando el comportamiento generado automáticamente por el compilador no sea el deseado.
Para ello, la definición del constructor por defecto se corresponde con la definición de un constructor
que no recibe ningún parámetro, y la implementación 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 continuación se puede ver como sería la implementación del constructor por defecto:
Otra posible implementación podría ser la siguiente, que invoca explícitamente al constructor por
defecto para cada atributo miembro de la clase (que en este caso se incializará a cero):
Finalmente, a continuación podemos ver un ejemplo de como sería una invocación a dicho cons-
tructor por defecto:
int main()
{ real: 0.0 real: 0.0
Complejo c1, c2 ; imag: 0.0 imag: 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 automáticamente al inicializar el contenido
de un objeto con el valor de otro objeto de su misma clase, y también es invocado automáticamente
cuando un objeto de dicho tipo se pasa como parámetro 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 método especial de la clase, ya que si el programador no define
dicho constructor de copia para una determinada clase, entonces el compilador generará e im-
plementará automáticamente dicho constructor de copia con el comportamiento por defecto de
invocar automáticamente 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 definir el constructor de copia para una determinada clase
cuando el comportamiento generado automáticamente por el compilador no sea el deseado. Para
ello, la definición del constructor de copia se corresponde con la definición de un constructor
que recibe como único parámetro por referencia constante un objeto del mismo tipo que la clase
del constructor, y la implementación dependerá de las acciones necesarias para copiar el estado
interno del objeto recibido como parámetro al objeto que se está creando. Por ejemplo, para la
clase Complejo:
class Complejo {
public:
Complejo(const Complejo& c) ; // Constructor de Copia
} ;
y su implementación podría ser la siguiente, que en este caso coincide con la implementación que
generaría automáticamente el compilador en caso de que no fuese implementado por el programa-
dor:
Finalmente, a continuación podemos ver un ejemplo de como sería una invocación al constructor de
copia (para c3 y c4), junto a una invocación a un constructor específico (para c2) y una invocación
al constructor por defecto (para c1), así como la construcción por copia (para c5 y c6) de objetos
construidos invocando explícitamente a los constructores adecuados:
Destructor
El destructor de una clase será invocado automáticamente (sin parámetros actuales) para una
determinada instancia (objeto) de esta clase cuando dicho objeto deba ser destruido, normalmente
ésto sucederá cuando el flujo de ejecución del programa salga del ámbito de visibilidad de dicho
objeto (véase 11.1.3).
El destructor es un método especial de la clase, ya que si el programador no define dicho
destructor para una determinada clase, entonces el compilador generará e implementará automá-
ticamente dicho destructor con el comportamiento por defecto de invocar automáticamente al
destructor para cada atributo de tipo compuesto miembro de la clase.
No obstante, el programador puede definir el destructor para una determinada clase cuando
el comportamiento generado automáticamente por el compilador no sea el deseado. Para ello, el
destructor de la clase se define mediante el símbolo ~ seguido del identificador de la clase y una
lista de parámetros vacía, y la implementación dependerá de las acciones necesarias para destruir
y liberar los recursos asociados al estado interno del objeto que se está destruyendo. Nótese que la
definición del constructor no especifica ningún tipo de valor devuelto (ni siquiera void).
Posteriormente, el destructor invoca automáticamente a los destructores de los atributos miem-
bros del objeto para que éstos sean destruidos. Por ejemplo, para la clase Complejo:
class Complejo {
public:
~Complejo() ; // Destructor
} ;
y su implementación podría ser la siguiente, que en este caso coincide con la implementación que
generaría automáticamente el compilador en caso de que no fuese implementado por el programa-
dor:
int main()
imag:
0.0
0.0 Xreal:
imag:
2.5
7.3 X real:
imag:
0.0
0.0 X
real:
imag: X
2.5
7.3
c1 c2 c3 c4
{
Complejo c1 ; // Construcción por defecto
Complejo c2(2.5, 7.3) ; // Construcción específica
Complejo c3(c1) ; // Construcción de copia (de c1)
Complejo c4 = c2 ; // Construcción de copia (de c2)
// ...
} // Destrucción automática de c4, c3, c2 y c1
//- fin: main.cpp ---------------------------------------------------
Operador de Asignación
El operador de asignación define como se realiza la asignación (=) para objetos de esta clase.
No se debe confundir el operador de asignación con el constructor de copia, ya que el constructor
de copia construye un nuevo objeto que no tiene previamente ningún valor, mientras que en el caso
del operador de asignación, 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 asignación
del nuevo valor.
El operador de asignación (=) es un método especial de la clase, ya que si el programador no
define dicho operador de asignación para una determinada clase, entonces el compilador generará e
implementará automáticamente dicho operador de asignación con el comportamiento por defecto
de invocar automáticamente al operador de asignación para cada atributo miembro de la clase,
tanto para atributos de tipo simple como de tipo compuesto.
No obstante, el programador puede definir el operador de asignación para una determinada
clase cuando el comportamiento generado automáticamente por el compilador no sea el deseado.
Para ello, la definición del operador de asignación se corresponde con la definición de un operador
= que recibe como único parámetro por referencia constante un objeto del mismo tipo que la clase
del constructor, devuelve una referencia al propio objeto que recibe la asignación, y la implemen-
tación dependerá de las acciones necesarias para destruir el estado interno del objeto que recibe
la asignación y para asignar el estado interno del objeto recibido como parámetro al objeto que se
está creando. Por ejemplo, para la clase Complejo:
class Complejo {
public:
Complejo& operator=(const Complejo& o) ; // Operador de Asignación
} ;
y su implementación podría ser la siguiente, que en este caso coincide con la implementación que
generaría automáticamente el compilador en caso de que no fuese implementado por el programa-
dor:
Complejo& Complejo::operator=(const Complejo& o) // Operador de Asignacion
{ // Implementación automática
real = o.real ;
imag = o.imag ;
return *this ;
}
El operador de asignación debe devolver el objeto actual (return *this) sobre el que recae la
asignación.
Finalmente, a continuación podemos ver un ejemplo de como sería una invocación al operador
de asignación (para c3 y c4), junto a una invocación a un constructor específico (para c2) y una
invocación al constructor por defecto (para c1), así como la asignación (para c5 y c6) de objetos
construidos invocando explícitamente a los constructores adecuados:
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 asignación
antes de asignar el nuevo valor. En este caso, es conveniente comprobar que no se está produciendo
una auto-asignación del mismo objeto (x = x), ya que en este caso se destruiría la representación
interna del objeto antes de haberla asignado, con los errores que ello trae asociado. Por lo tanto,
suele ser habitual que el operador de asignación implemente una condición para evitar la asignación
en el caso de que se produzca una auto-asignación, 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 dirección en memoria del objeto que recibe la asignación, y &o representa la
dirección en memoria del objeto que se recibe como parámetro. Si ambas direcciones son diferentes,
entonces significa que son variables diferentes y se puede realizar la asignación.
11.2.1. Ejemplo
Veamos un Tipo Abstracto de Datos Lista de enteros, la cual permite almacenar una secuencia
de número enteros, permitiendo insertar, eliminar, acceder y modificar elementos según la posición
que ocupen en la secuencia de números.
Definición
//- fichero: lista.hpp ------------------------------------------
#ifndef lista_hpp_
#define lista_hpp_
#include <array>
namespace umalcc {
class ListaInt {
public:
//----------------------------------------------------------
//-- Métodos Públicos --------------------------------------
//----------------------------------------------------------
// ~ListaInt() ; // Destructor Automático
//------------------------------
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::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 ----------------------------------------------
Implementación
//- fichero: lista.cpp ------------------------------------------
#include "lista.hpp"
#include <cassert>
namespace umalcc {
//----------------------------------------------------------
//-- Métodos Públicos --------------------------------------
//----------------------------------------------------------
// ListaInt::~ListaInt() { } // Destructor Automático
//----------------------------------
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] ;
}
}
//----------------------------------
ListaInt& ListaInt::operator = (const ListaInt& o) // Op. de Asignación
{
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) {
v[i] = v[i-1];
}
++sz; // Ahora hay un elemento más
}
//----------------------------------
void ListaInt::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];
}
}
//----------------------------------
}
//- fin: lista.cpp ----------------------------------------------
Utilización
//- 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) << " " ;
}
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 ;
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 ---------------------------------------------------
Hasta ahora, todos los programas que se han visto en capítulos anteriores almacenan su estado
interno por medio de variables que son automáticamente gestionadas por el compilador. Las varia-
bles son creadas cuando el flujo de ejecución entra en el ámbito de su definición (se reserva espacio
en memoria y se crea el valor de su estado inicial), posteriormente se manipula el estado de la varia-
ble (accediendo o modificando su valor almacenado), y finalmente se destruye la variable cuando el
flujo de ejecución 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 automáticamente por el
compilador se las suele denominar variables automáticas (también variables locales), y residen en
una zona de memoria gestionada automáticamente por el compilador, la pila de ejecución, donde
se alojan y desalojan las variables locales (automáticas) pertenecientes al ámbito de ejecución de
cada subprograma.
Así, el tiempo de vida de una determinada variable está condicionado por el ámbito de su decla-
ración. Además, el número de variables automáticas utilizadas en un determinado programa está
especificado explícitamente en el propio programa, y por lo tanto su capacidad de almacenamiento
está también especificada y predeterminada por lo especificado explícitamente en el programa.
Es decir, con la utilización única de variables automáticas, la capacidad de almacenamiento de
un determinado programa está predeterminada desde el momento de su programación (tiempo de
compilación), y no puede adaptarse a las necesidades reales de almacenamiento surgidas durante
la ejecución del programa (tiempo de ejecución).1
La gestión de memoria dinámica surge como un mecanismo para que el propio programa, du-
rante su ejecución (tiempo de ejecución), pueda solicitar (alojar) y liberar (desalojar) memoria
según las necesidades surgidas durante una determinada ejecución, dependiendo de las circuns-
tancias reales de cada momento de la ejecución del programa en un determinado entorno. Esta
ventaja adicional viene acompañada por un determinado coste asociado a la mayor complejidad
que requiere su gestión, ya que en el caso de las variables automáticas, es el propio compilador el
encargado de su gestión, sin embargo en el caso de las variables dinámicas es el propio programa-
dor el que debe, mediante código software, gestionar el tiempo de vida de cada variable dinámica,
cuando debe ser alojada y creada, como será utilizada, y finalmente cuando debe ser destruida
y desalojada. Adicionalmente, como parte de esta gestión de la memoria dinámica por el propio
programador, la memoria dinámica pasa a ser un recurso que debe gestionar el programador, y se
debe preocupar de su alojo y de su liberación, 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 invocación
recursiva en tiempo de ejecución tiene la capacidad de alojar nuevas variables que serán posteriormente desalojadas
automáticamente cuando la llamada recursiva finaliza.
155
156 CAPÍTULO 12. MEMORIA DINÁMICA. PUNTEROS
12.1. Punteros
El tipo puntero es un tipo simple que permite a un determinado programa acceder a posiciones
concretas de memoria, y más específicamente a determinadas zonas de la memoria dinámica. Aun-
que el lenguaje de programación C++ permite otras utilizaciones más diversas del tipo puntero,
en este capítulo sólo se utilizará el tipo puntero para acceder a zonas de memoria dinámica.
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 dinámica. Por lo tanto, para un
determinado tipo puntero, se debe especificar también el tipo de la variable (en memoria dinámica)
a la que apunta, el cual define el espacio que ocupa en memoria y las operaciones (y métodos) que
se le pueden aplicar, entre otras cosas.
De este modo, cuando un programa gestiona la memoria dinámica a través de punteros, debe
manejar y gestionar por una parte la propia variable de tipo puntero, y por otra parte la variable
dinámica apuntada por éste.
Un tipo puntero se define utilizando la palabra reservada typedef seguida del tipo de la variable
dinámica apuntada, un asterisco para indicar que es un puntero a una variable de dicho tipo, y
el identificador que denomina al tipo. Por ejemplo:
typedef int* PInt ; // Tipo Puntero a Entero
int main()
{
PPersona ptr = new Persona("pepe", "111", 5) ;
Sin embargo, si una variable de tipo puntero tiene el valor NULL, entonces desreferenciar la variable
produce un error en tiempo de ejecución que aborta la ejecución del programa. Así mismo, des-
referenciar un puntero con valor inespecificado produce un comportamiento anómalo en tiempo
de ejecución.
Es posible, así mismo, acceder a los elementos de la variable apuntada mediante el operador de
desreferenciación. Por ejemplo:
int main()
{
PPersona ptr = new Persona ;
(*ptr).nombre = "pepe" ;
(*ptr).telefono = "111" ;
(*ptr).edad = 5 ;
delete ptr ;
}
Nótese que el uso de los paréntesis es obligatorio debido a que el operador punto (.) tiene mayor
precedencia que el operador de desreferenciación (*). Por ello, en el caso de acceder a los campos
de un registro en memoria dinámica a través de una variable de tipo puntero, es más adecuado
utilizar el operador de desreferenciación (->). Por ejemplo:
int main()
{
PPersona ptr = new Persona ;
ptr->nombre = "pepe" ;
ptr->telefono = "111" ;
ptr->edad = 5 ;
delete ptr ;
}
Este operador también se utiliza para invocar a métodos de un objeto si éste se encuentra alojado
en memoria dinámica. 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()
{
delete ptr ;
}
dinámica, donde se declara que un determinado identificador es una estructura o clase, pero no se
definen sus componentes.
struct Nodo ;
Insertar al Principio typedef Nodo* PNodo ;
struct Nodo {
lista: • - PNodo sig ;
maría string dato ;
} ;
PNodo ptr = new Nodo("pepe") ; void insertar_principio(PNodo& lista, const string& dt)
lista: • - {
maría PNodo ptr = new Nodo ;
ptr: •−−→ • ptr->dato = dt ;
pepe ptr->sig = lista ;
lista = ptr ;
ptr->sig = lista ;
}
lista: • -
3
maría void insertar_final(PNodo& lista, const string& dt)
ptr: •−−→ • {
pepe PNodo ptr = new Nodo ;
lista = ptr ; ptr->dato = dt ;
ptr->sig = NULL ;
lista: • if (lista == NULL) {
@ 3
R
@ maría lista = ptr ;
ptr: •−−→ • } else {
pepe PNodo act = lista ;
while (act->sig != NULL) {
act = act->sig ;
lista: •−−→ •−−−−−→ }
pepe maría act->sig = ptr ;
}
}
Insertar Detrás
PNodo situar(PNodo lista, int pos)
ant: •−−→ • - {
pepe maría int i = 0;
PNodo ptr = lista;
while ((ptr != NULL)&&(i < pos)) {
PNodo ptr = new Nodo("juan") ; ptr = ptr->sig;
ant: •−−→ • - ++i;
pepe maría }
ptr: • - • return ptr;
juan }
ptr->sig = ant->sig ; void insertar_pos(PNodo& lista, int pos, const string& dt)
ant: •−−→ • - {
3
pepe maría if (pos < 1) {
ptr: • - • PNodo ptr = new Nodo ;
juan ptr->dato = dt ;
ptr->sig = lista ;
ant->sig = ptr ;
lista = ptr ;
ant: •−−→ •Q } else {
3
pepe Q maría PNodo ant = situar(lista, pos - 1);
ptr: •
s
Q
- • if (ant != NULL) {
juan PNodo ptr = new Nodo ;
ptr->dato = dt ;
ptr->sig = ant->sig ;
ant: •−−→ •−−−−−→ •−−−
−−→ ant->sig = ptr ;
pepe juan maría }
}
}
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, int d) { ... }
void eliminar(int pos) { ... }
// ...
private:
struct Nodo ;
typedef Nodo* PNodo ;
struct Nodo {
PNodo sig ;
int dato ;
} ;
//-- Métodos privados --
void destruir(PNodo& l) const { ... }
PNodo duplicar(PNodo l) const { ... }
// ...
//-- Atributos privados --
int sz ;
PNodo lista ;
} ;
Introducción a la Programación
Orientada a Objetos
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 representación in-
terna del objeto, cuyo acceso está restringido, y los Métodos permiten la manipulación e interacción
entre objetos. Así, una Clase define una abstracción, y los métodos definen su comportamiento. Así,
las características de un determinado objeto, su estado y su comportamiento, están determinadas
por la clase a la que pertenece. Del mismo modo, el objeto podrá ser manipulado e interactuar con
otros objetos a través de los métodos definidos por la Clase a la que pertenece.
La Programación Orientada a Objetos proporciona un mecanismo adecuado para el diseño y
desarrollo de software complejo, modular, reusable, adaptable y extensible.
167
168 CAPÍTULO 13. INTRODUCCIÓN A LA PROGRAMACIÓN ORIENTADA A OBJETOS
El polimorfismo permite que un objeto de una clase derivada pueda ser considerado y utilizado
como si fuera un objeto de la clase base, proporcionando un soporte adecuado para el Principio
de sustitución, mediante el cual, un objeto de la clase derivada puede sustituir a un objeto de la
clase base, allí donde sea necesario. Sin embargo, la dirección de correspondencia opuesta no se
mantiene, ya que no todos los objetos de la clase base son también objetos de la clase derivada.
La vinculación dinámica permite que las clases derivadas puedan redefinir el comportamiento
de los métodos definidos en la clase base. Así, en contextos polimórficos, gracias a la vinculación di-
námica, los métodos invocados se seleccionan adecuadamente, en tiempo de ejecución, dependiendo
del tipo real del objeto, y no del tipo aparente. Es decir, si se invoca a un determinado método sobre
un objeto de una clase derivada que haya redefinido la implementacion de ese método, entonces
se ejecutará el código del método redefinido en la clase derivada, incluso aunque la invocación se
haya producido en un contexto polimórfico donde el objeto de la clase derivada haya sustituido a
un objeto de la clase base.
class Base {
// ...
};
class Derivada : public Base {
// ...
};
Así mismo, también es posible definir un nuevo ámbito de visibilidad (protected) como un ámbito
de acceso restringido que permite el acceso desde la propia clase, así como también desde las clases
derivadas. De esta forma se proporciona un ambito adecuado donde definir métodos protegidos en
la clase base para que las clases derivadas puedan manipular adecuadamente el estado interno de
la clase base cuando sea necesario.
Los constructores de las clases derivadas pueden invocar explícitamente, al principio de la lista
de inicialización, a los contructores de las clases base. En caso de que el constructor de la clase base
no sea invocado explícitamente desde la lista de inicialización del constructor de la clase derivada,
Así, la clase polimórfica Automovil hereda tanto los atributos (ident y posicion) como los
métodos (id(), estacionar(), mover() y clone()) de la clase base vehiculo, pero redefine
los métodos mover() y clone() de la clase base, proporcionando una nueva implementación y
comportamiento. Además, añade un nuevo atributo (deposito) y un nuevo método (repostar())
a la clase Automovil.
Se puede apreciar como tanto el constructor específico como el constructor de copia de la clase
Automovil invocan en la lista de inicialización, en orden, tanto al constructor de la clase base,
como al constructor de cada atributo.
Sin embargo, la dirección de correspondencia opuesta no se mantiene, ya que no todos los punteros
a objetos de la clase base son también punteros a objetos de la clase derivada. Por ejemplo:
Automovil* ptr_automovil = ptr_vehiculo ; // Error ptr_vehiculo apunta a un objeto Bicicleta
Para realizar la conversión opuesta, hay que utilizar una operación especial de casting dinámico,
que comprueba en tiempo de ejecución si el objeto real puede ser convertido al tipo que se solicita:
Bicicleta* ptr_bicicleta = dynamic_cast<Bicicleta*>(ptr_vehiculo) ;
Si el tipo real del objeto puede ser convertido al tipo especificado en la conversión, entonces el
operador dynamic_cast<>() produce un puntero al objeto del tipo especificado. Sin embargo, si la
condición anterior no se cumple, y la conversión de tipos no es adecuada, entonces devuelve NULL.
Bicicleta* ptr_bicicleta = dynamic_cast<Bicicleta*>(ptr_vehiculo) ;
if (ptr_bicicleta != NULL) {
ptr_bicicleta->cambiar();
}
La vinculación dinámica permite que las clases derivadas puedan redefinir el comportamiento
de los métodos definidos en la clase base. Así, la vinculación dinámica permite, en contextos
Pero la siguiente invocación al método mover() a través de un puntero de tipo Vehiculo* tambien
ejecutará la definición proporcionada por la clase Bicicleta, ya que en realidad, ptr_vehiculo
es un puntero a un objeto polimórfico creado como un objeto de tipo real Bicicleta:
ptr_vehiculo->mover() ;
Las invocaciones a los métodos id(), estacionar() y mover() son adecuadas gracias a la
vinculación dinámica, e invocarán a las implementaciones correspondientes dependiendo del
tipo real del objeto, y no del tipo aparente del puntero.
Las invocaciones a los métodos id() y estacionar() se pueden realizar tanto a través de
punteros a Vehiculo, como a través de punteros a Bicicleta y Automovil, gracias a que
estas clases han heredado dichos métodos de la clase base.
La invocación al método cambiar() o repostar() sólo puede realizarse a través de punteros
a la clase Bicicleta o Automovil (y derivadas), pero no a través de punteros a la clase
Vehiculo, ya que esta clase no proporciona de tales métodos.
Un puntero a un objeto de la clase Bicicleta o de la clase Automovil puede ser asignado a
un puntero de la clase Vehiculo gracias al polimorfismo.
El operador dynamic_cast<>() permite convertir un puntero a un objeto de la clase base a
un puntero a un objeto de la clase derivada si y solo si el tipo real del objeto es de la clase
derivada, o alguno de sus derivados.
El método clone() realiza una duplicación y copia del objeto real gracias a la vinculación
dinámica.
La invocación a delete destruye cada objeto adecuadamente según su tipo real gracias a la
vinculación dinámica.
13.4. Ejemplo
A continuación se muestra la definición de la clase no-polmórfica Parking que permite aparcar
los vehículos de la jerarquía de clases definida anteriormente.
#ifndef parking_hpp_
#define parking_hpp_
#include "vehiculo.hpp"
#include <array>
#include <string>
namespace umalcc {
class Parking { // CLASE NO-POLIMORFICA
public:
~Parking();
Parking();
Parking(const Parking& o);
Parking& operator=(const Parking& o);
void mostrar() const;
void anyadir(Vehiculo* v, bool& ok);
Vehiculo* extraer(const std::string& id);
private:
static const int MAX = 100;
typedef std::array<Vehiculo*, MAX> Park;
//--
int buscar(const std::string& id) const;
void destruir();
void copiar(const Parking& o);
//--
int n_v;
Park parking;
};
}
#endif
A continuación se muestra la implementación de la clase no-polmórfica Parking que permite
aparcar los vehículos de la jerarquía de clases definida anteriormente. En ella, se puede apreciar
como se invoca al método clone() cuando se va a duplicar un objeto de la clase Parking. Así
mismo, también se puede apreciar como se invocan a los metodos id(), estacionar() y mover()
según sea necesario.
Finalmente, el siguiente código muestra como se crean diferentes objetos de las clases definidas
anteriomente, se almacenan en un aparcamiento, y se realizan diferentes manipulaciones de los ob-
jetos. Nótese como el destructor del Parking se encargara de destruir los objetos allí almacenados.
#include "vehiculo.hpp"
#include "bicicleta.hpp"
#include "automovil.hpp"
#include "parking.hpp"
#include <iostream>
using namespace std;
using namespace umalcc;
int main()
{
bool ok;
Parking park;
Automovil* a1 = new Automovil("A1");
Bicicleta* b1 = new Bicicleta("B1");
Vehiculo* a2 = new Automovil("A2");
park.anyadir(a1, ok);
park.anyadir(b1, ok);
park.anyadir(a2, ok);
Parking aux = park;
Vehiculo* v = aux.extraer("A1");
if (v != NULL) {
cout << "Vehiculo: " << v->id() << endl;
v->mover();
}
aux.mostrar();
Automovil* ax = dynamic_cast<Automovil*>(v);
if (ax != NULL) {
ax->repostar(20);
}
park.mostrar();
delete v;
}
177
178CAPÍTULO 14. INTRODUCCIÓN A LOS CONTENEDORES DE LA BIBLIOTECA ESTÁNDAR (STL)
14.1. Vector
El contenedor de tipo vector<...> representa una secuencia de elementos homogéneos optimi-
zada para el acceso directo a los elementos según su posición, así como también para la inserción
de elementos al final de la secuencia y para la eliminación de elementos del final de la secuencia.
Para utilizar un contenedor de tipo vector se debe incluir la biblioteca estándar <vector>, de tal
forma que sus definiciones 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 tamaño puede crecer en tiempo de ejecución dependiendo de las necesidades surgidas
durante la ejecución del programa. Por ello, a diferencia de los arrays, no es necesario especificar
un tamaño fijo y predeterminado en tiempo de compilación respecto al número de elementos que
pueda contener.
El número máximo de elementos que se pueden almacenar en una variable de tipo vector
no está especificado, y se pueden almacenar elementos mientras haya capacidad suficiente en la
memoria del ordenador donde se ejecute el programa.
Nótese que en los siguientes ejemplos, por simplicidad, tanto el número de elementos como el
valor inicial de los mismos están especificados mediante valores constantes, sin embargo, también
se pueden especificar como valores de variables y expresiones calculados en tiempo de ejecución.
Las siguientes definiciones declaran el tipo Matriz como un vector de dos dimensiones de números
enteros.
int main()
{
Vect_Int v1 ; // vector de enteros vacío
std::vector<int> v2 ; // vector de enteros vacío
Matriz m ; // vector de dos dimensiones de enteros vacío
// ...
}
El constructor por defecto del tipo vector crea un objeto vector inicialmente vacío, sin elementos.
Posteriormente se podrán añadir y eliminar elementos cuando sea necesario.
También es posible crear un objeto vector con un número inicial de elementos con un valor inicial
por defecto, al que posteriormente se le podrán añadir nuevos elementos. Este número inicial de
elementos puede ser tanto una constante, como el valor de una variable calculado en tiempo de
ejecución.
int main()
{
Vect_Int v1(10) ; // vector con 10 enteros con valor inicial sin definir
Matriz m(10, Fila(5)) ; // matriz de 10x5 enteros con valor inicial sin definir
// ...
}
Así mismo, también se puede especificar el valor que tomarán 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
// ...
}
También es posible inicializar un vector con el contenido de otro vector de igual tipo, invocando al
constructor de copia:
int main()
{
Vect_Int v1(10, 3) ; // vector con 10 enteros con valor inicial 3
Vect_Int v2(v1) ; // vector con el mismo contenido de v1
Vect_Int v3 = v1 ; // vector con el mismo contenido de v1
Vect_Int v4 = Vect_Int(7, 5) ; // vector con 7 elementos de valor 5
// ...
}
int main()
{
Vect_Int v1(10, 3) ; // vector con 10 enteros con valor inicial 3
Vect_Int v2 ; // vector de enteros vacío
v2 = v1 ; // asigna el contenido de v1 a v2
v2.assign(5, 7) ; // asigna 5 enteros con valor inicial 7
v2 = Vect_Int(5, 7) ; // asigna un vector con 5 elementos de valor 7
}
Así mismo, también es posible intercambiar (swap en inglés) de forma eficiente el contenido entre
dos vectores utilizando el método swap. Por ejemplo:
int main()
{
Vect_Int v1(10, 5) ; // v1 = { 5, 5, 5, 5, 5, 5, 5, 5, 5, 5 }
Vect_Int v2(5, 7) ; // v2 = { 7, 7, 7, 7, 7 }
v1.swap(v2) ; // v1 = { 7, 7, 7, 7, 7 }
// v2 = { 5, 5, 5, 5, 5, 5, 5, 5, 5, 5 }
}
int main()
{
Vect_Int v1(10, 3) ; // vector con 10 enteros con valor inicial 3
int n = v1.size() ; // número de elementos de v1
}
También es posible cambiar el tamaño del número de elementos almacenados en el vector. Así,
el método resize(...) reajusta el número de elementos contenidos en un vector. Si el número
especificado es menor que el número actual de elementos, se eliminarán del final del vector tantos
elementos como sea necesario para reducir el vector hasta el número de elementos especificado. Si
por el contrario, el número especificado es mayor que el número actual de elementos, entonces se
añadirán al final del vector tantos elementos como sea necesario para alcanzar el nuevo número de
elementos especificado (con el valor especificado o con el valor por defecto). Por ejemplo:
int main()
{
Vect_Int v(10, 1) ; // v = { 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 }
v.resize(5) ; // v = { 1, 1, 1, 1, 1 }
v.resize(9, 2) ; // v = { 1, 1, 1, 1, 1, 2, 2, 2, 2 }
v.resize(7, 3) ; // v = { 1, 1, 1, 1, 1, 2, 2 }
v.resize(10) ; // v = { 1, 1, 1, 1, 1, 2, 2, 0, 0, 0 }
}
El lenguaje de programación C++ no comprueba que los accesos a los elementos de un vector
sean correctos y se encuentren dentro de los límites válidos del vector, por lo que será responsabi-
lidad del programador comprobar que así sea.
Sin embargo, en GNU G++, la opción de compilación -D_GLIBCXX_DEBUG permite comprobar
los índices de acceso.
También es posible acceder a un determinado elemento mediante el método at(i), de tal
forma que si el valor del índice i está fuera del rango válido, entonces se lanzará una excepción
out_of_range que abortará la ejecución del programa. Se puede tanto utilizar como modificar 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 ;
}
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 homogéneos optimi-
zada para el acceso directo a los elementos según su posición, así como también para la inserción
de elementos al principio y al final de la secuencia y para la eliminación de elementos del principio
y del final de la secuencia. Para utilizar un contenedor de tipo deque se debe incluir la biblioteca
estándar <deque>, de tal forma que sus definiciones se encuentran dentro del espacio de nombres
std:
#include <deque>
El contenedor deque presenta el mismo interfaz público que el contenedor vector, pero añade
dos métodos nuevos para facilitar la inserción y eliminación de elementos al principio de la secuencia
(push_front(...) y pop_front()).
El número máximo de elementos que se pueden almacenar en una variable de tipo deque no está
especificado, y se pueden almacenar elementos mientras haya capacidad suficiente en la memoria
del ordenador donde se ejecute el programa.
El constructor por defecto del tipo deque crea un objeto deque inicialmente vacío, sin elementos.
Posteriormente se podrán añadir y eliminar elementos cuando sea necesario.
También es posible crear un objeto deque con un número inicial de elementos con un valor inicial
por defecto, al que posteriormente se le podrán añadir nuevos elementos. Este número inicial de
elementos puede ser tanto una constante, como el valor de una variable calculado en tiempo de
ejecución.
int main()
{
Deque_Int v1(10) ; // deque con 10 enteros con valor inicial sin definir
// ...
}
Así mismo, también se puede especificar el valor que tomarán los elementos creados inicialmente.
int main()
{
Deque_Int v1(10, 3) ; // deque con 10 enteros con valor inicial 3
// ...
}
También es posible inicializar un deque con el contenido de otro deque de igual tipo, invocando al
constructor de copia:
int main()
{
Deque_Int v1(10, 3) ; // deque con 10 enteros con valor inicial 3
Deque_Int v2(v1) ; // deque con el mismo contenido de v1
Deque_Int v3 = v1 ; // deque con el mismo contenido de v1
Deque_Int v4 = Deque_Int(7, 5) ; // deque con 7 elementos de valor 5
// ...
}
v2 = v1 ; // asigna el contenido de v1 a v2
v2.assign(5, 7) ; // asigna 5 enteros con valor inicial 7
v2 = Deque_Int(5, 7) ; // asigna un deque con 5 elementos de valor 7
}
Así mismo, también es posible intercambiar (swap en inglés) de forma eficiente el contenido entre
dos deques utilizando el método swap. Por ejemplo:
int main()
{
Deque_Int v1(10, 5) ; // v1 = { 5, 5, 5, 5, 5, 5, 5, 5, 5, 5 }
Deque_Int v2(5, 7) ; // v2 = { 7, 7, 7, 7, 7 }
v1.swap(v2) ; // v1 = { 7, 7, 7, 7, 7 }
// v2 = { 5, 5, 5, 5, 5, 5, 5, 5, 5, 5 }
}
} // v = { 0, 0, 0 }
v.clear() ; // v = { }
}
También es posible cambiar el tamaño del número de elementos almacenados en el deque. Así,
el método resize(...) reajusta el número de elementos contenidos en un deque. Si el número
especificado es menor que el número actual de elementos, se eliminarán del final del deque tantos
elementos como sea necesario para reducir el deque hasta el número de elementos especificado. Si
por el contrario, el número especificado es mayor que el número actual de elementos, entonces se
añadirán al final del deque tantos elementos como sea necesario para alcanzar el nuevo número de
elementos especificado (con el valor especificado o con el valor por defecto). Por ejemplo:
int main()
{
Deque_Int v(10, 1) ; // v = { 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 }
v.resize(5) ; // v = { 1, 1, 1, 1, 1 }
v.resize(9, 2) ; // v = { 1, 1, 1, 1, 1, 2, 2, 2, 2 }
v.resize(7, 3) ; // v = { 1, 1, 1, 1, 1, 2, 2 }
v.resize(10) ; // v = { 1, 1, 1, 1, 1, 2, 2, 0, 0, 0 }
}
14.3. Stack
El adaptador de contenedor de tipo stack<...> representa el tipo abstracto de datos Pila,
como una colección ordenada (según el orden de inserción) de elementos homogéneos donde se
pueden introducir elementos (manteniendo el orden de inserción) y sacar elementos de ella (en
orden inverso al orden de inserción), de tal forma que el primer elemento que sale de la pila es el
último elemento que ha sido introducido en ella. Además, también es posible comprobar si la pila
contiene elementos, de tal forma que no se podrá sacar ningún elemento de una pila vacía. Para
utilizar un adaptador de contenedor de tipo stack se debe incluir la biblioteca estándar <stack>,
de tal forma que sus definiciones se encuentran dentro del espacio de nombres std:
#include <stack>
El número máximo de elementos que se pueden almacenar en una variable de tipo stack no está
especificado, y se pueden introducir elementos mientras haya capacidad suficiente en la memoria
del ordenador donde se ejecute el programa.
El constructor por defecto del tipo stack crea un objeto stack inicialmente vacío, sin elementos.
Posteriormente se podrán añadir y eliminar elementos cuando sea necesario.
También es posible inicializar una pila con el contenido de otra pila de igual tipo:
int main()
{
Stack_Int s1 ; // stack de enteros vacío
// ...
Stack_Int s2(s1) ; // stack con el mismo contenido de s1
Stack_Int s3 = s1 ; // stack con el mismo contenido de s1
Stack_Int s4 = Stack_Int() ; // copia el contenido de stack vacío
// ...
}
s2 = s1 ; // asigna el contenido de s1 a s2
s2 = Stack_Int() ; // asigna el contenido de stack vacío
}
s.top() = 5 ; // s = { 1, 2, 5 }
s.pop() ; // s = { 1, 2 }
s.pop() ; // s = { 1 }
s.push(7) ; // s = { 1, 7 }
s.push(9) ; // s = { 1, 7, 9 }
while (! s.empty()) {
cout << s.top() << " " ; // muestra: 9 7 1
s.pop() ;
} // s = { }
cout << endl ;
}
14.4. Queue
El adaptador de contenedor de tipo queue<...> representa el tipo abstracto de datos Cola,
como una colección ordenada (según el orden de inserción) de elementos homogéneos donde se
pueden introducir elementos (manteniendo el orden de inserción) y sacar elementos de ella (en el
mismo orden al orden de inserción), de tal forma que el primer elemento que sale de la cola es el
primer elemento que ha sido introducido en ella. Además, también es posible comprobar si la cola
contiene elementos, de tal forma que no se podrá sacar ningún elemento de una cola vacía. Para
utilizar un adaptador de contenedor de tipo queue se debe incluir la biblioteca estándar <queue>,
de tal forma que sus definiciones se encuentran dentro del espacio de nombres std:
#include <queue>
El número máximo de elementos que se pueden almacenar en una variable de tipo queue no está
especificado, y se pueden introducir elementos mientras haya capacidad suficiente en la memoria
del ordenador donde se ejecute el programa.
int main()
{
Queue_Int c1 ; // queue de enteros vacío
std::queue<int> c2 ; // queue de enteros vacío
// ...
}
El constructor por defecto del tipo queue crea un objeto queue inicialmente vacío, sin elementos.
Posteriormente se podrán añadir y eliminar elementos cuando sea necesario.
También es posible inicializar una cola con el contenido de otra cola de igual tipo:
int main()
{
Queue_Int c1 ; // queue de enteros vacío
// ...
Queue_Int c2(c1) ; // queue con el mismo contenido de c1
Queue_Int c3 = c1 ; // queue con el mismo contenido de c1
Queue_Int c4 = Stack_Int() ; // copia el contenido de queue vacío
// ...
}
c2 = c1 ; // asigna el contenido de c1 a c2
c2 = Queue_Int() ; // asigna el contenido de queue vacío
}
c.front() = 6 ; // c = { 6, 2, 3 }
c.back() = 5 ; // c = { 6, 2, 5 }
c.pop() ; // c = { 2, 5 }
c.pop() ; // c = { 5 }
c.push(7) ; // c = { 5, 7 }
c.push(9) ; // c = { 5, 7, 9 }
while (! c.empty()) {
cout << c.front() << " " ; // muestra: 5 7 9
c.pop() ;
} // c = { }
cout << endl ;
}
struct Agente {
string nombre ;
double ventas ;
} ;
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) ;
}
}
int main ()
{
VAgentes v ;
leer(v) ;
purgar(v, media(v)) ;
imprimir(v) ;
}
//------------------------------------------------------------------
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] ;
}
}
}
int main()
{
Matriz m1, m2, m3 ;
leer(m1) ;
leer(m2) ;
multiplicar(m1, m2, m3) ;
if (m3.size() == 0) {
cout << "Error en la multiplicación de Matrices" << endl ;
} else {
imprimir(m3) ;
}
}
//------------------------------------------------------------------
Bibliografía
193
Índice alfabético
declaración operadores, 13
global, 31 aritméticos, 19
ámbito de visibilidad, 31 bits, 19
local, 32 condicional, 20
ámbito de visibilidad, 32 lógicos, 20
declaracion adelantada, 160 relacionales, 19
definición ordenación
vs. declaración, 15 burbuja, 90
delete, 157 inserción, 90
delimitadores, 13 intercambio, 90
194
ÍNDICE ALFABÉTICO 195
selección, 89 bool, 16
char, 16
parámetros de entrada, 48 double, 16
parámetros de entrada/salida, 49 float, 16
parámetros de salida, 48 int, 16
paso por referencia constante, 55 long, 16
paso por referencia, 49 long long, 16
paso por valor, 48
procedimientos, 45 using namespace, 124
declaración, 51
inline, 51 variables
prototipo, 51 declaración, 18
registro, 64
return, 47
salida, 26
secuencia de sentencias, 31
sentencia
asignación, 20
incremento/decremento, 21
iteración, 37
do while, 40
for, 38
while, 37
selección, 32
if, 33
switch, 35
tipo, 15
tipos
cuadro resumen, 16
puntero, 156
acceso, 158
operaciones, 158
parámetros, 160
tipos simples
predefinidos
short, 16
unsigned, 16
tipos compuestos
array, 68
tipos simples
escalares, 16
tipos compuestos
array, 68
struct, 64
tipos simples
predefinidos, 16
tipos compuestos, 15, 55
parámetros, 55
tipos simples, 15
enumerado, 17
ordinales, 16
predefinidos