Documentos de Académico
Documentos de Profesional
Documentos de Cultura
Curso 2017-18
Capı́tulo 1
Problemas de tratamiento de
información, algoritmos y programas
En este capı́tulo se ilustra cómo son los programas que permiten resolver problemas de
tratamiento de información al ser ejecutados por un computador. Se analizan los primeros
problemas de tratamiento de información de este curso y se presentan programas que los
resuelven, explicando cada uno de sus elementos.
Los computadores, que ası́ van a ser denominados en este curso, son máquinas concebidas
para resolver problemas de tratamiento de información de forma automática y veloz. El
comportamiento de un computador viene regido por programas cuya ejecución debe proporcionar
la solución de los problemas que se pretenden resolver.
2
tratamiento de información considerado.
En este curso se va a utilizar el lenguaje de programación C++ como notación para describir
algoritmos. De igual forma se podrı́a utilizar otro lenguaje de programación o un lenguaje
natural.
Debe quedar claro que el objetivo de nuestro aprendizaje no va a centrarse en el lenguaje C++,
sino en los conceptos y técnicas de programación que van a ser presentados y en la aplicación
de una buena metodologı́a de diseño de programas.
Vamos a presentar alguno de los conceptos más básicos sobre la estructura, funcionamiento
y puesta a punto de un programa C++ siguiendo como guion una colección de programas muy
simples, aunque de complejidad creciente. En capı́tulos posteriores tendremos oportunidad de
profundizar en dichos conceptos.
Un programa C++ consta, como mı́nimo, de una función denominada main(). Al invocar el
programa se ejecuta el código de su función main(). Su ejecución termina al concluir la ejecución
del código de dicha función.
Este primer programa se limita a dar la bienvenida a la Universidad escribiendo para ello un
mensaje por pantalla.
#include <iostream>
/∗
∗ Pre: −−−
∗ Post: Escribe por pantalla el mensaje ”Bienvenidos a la Universidad”
∗/
int main() {
std :: cout << ”Bienvenidos a la Universidad” << std::endl; // primera instrucción
return 0; // segunda instrucción
3
}
El programa se completa con las cuatro lı́neas finales que describen la cabecera de la función
main() y su bloque de instrucciones ejecutables, encerrado entre un par de llaves, { . . . }.
La cabecera de la función main() comienza con la declaración del tipo de dato que devuelve
al concluir su ejecución, en este caso un dato entero de tipo int.
Para facilitar el desarrollo de grandes proyectos software, C++ permite definir diferentes
espacios de nombres (namespace). Ello permitirá al programador distinguir dos o más
elementos diferentes a los que se les haya asociado un nombre idéntico.
Hay un espacio de nombres global al que pertenecen todos los elementos del programa
que no han sido definidos dentro de un espacio de nombres especı́fico.
Podemos evitar tener que prefijar los nombres de los elementos de un espacio de nombres con
el nombre de dicho espacio mediante la inclusión de una cláusula using namespace, tal como
se muestra a continuación. Si tenemos en cuenta el uso de la biblioteca estándar iostream en
la práctica totalidad de nuestros programas, entenderemos por qué se suele incluir la cláusula
using namespace std en buena parte de todos ellos.
4
#include <iostream>
/∗
∗ Pre: −−−
∗ Post: Escribe por pantalla el mensaje ”Bienvenidos a la Universidad”
∗/
int main() {
// Presenta un mensaje por el dispositivo estándar de salida (pantalla)
cout << ”Bienvenidos a la Universidad” << endl;
// La ejecución del programa termina normalmente
return 0;
}
La gestión de estos pasos depende del sistema operativo que gobierna el uso del computador
utilizado y de las herramientas de desarrollo de las que se disponga. En un sistema unix, ésta
serı́a la secuencia de tareas para poner a punto el programa anterior:
Una vez editado el código del programa en el fichero bienvenida.cc, ésta es la secuencia de
órdenes para obtener un fichero saludo que almacene el correspondiente programa ejecutable:
5
Una vez construido el programa ejecutable saludo, éste puede ser ejecutado cuantas veces se
desee, exhibiendo el programa un comportamiento idéntico en cada ejecución.
computador $ ./saludo
Bienvenidos a la Universidad
computador $ ./saludo
Bienvenidos a la Universidad
computador $ ./saludo
Bienvenidos a la Universidad
computador $ ...
Hay un buen número de IDEs para desarrollar software en C++ como, por ejemplo, Code
Blocks, CodeLite, Dev-C++, Eclipse o NetBeans.
Dos tipos de comentarios documentan el código del programa bienvenida.cc. Los primeros
comienzan con la secuencia de caracteres /* y concluyen con la secuencia */. Pueden tener
cualquier longitud y extenderse a lo largo de una o más lı́neas. Los segundos comienzan por la
secuencia de caracteres // y concluyen al finalizar la misma lı́nea en la que están ubicados.
El programa que se presenta a continuación informa por pantalla de los valores del radio y
la longitud de varias circunferencias.
Con él se pretende ilustrar cómo un programa C++ suele constar de una colección de funciones:
la función principal main() y algunas funciones auxiliares. Cada una de sus funciones puede ser
invocada una o varias veces desde diferentes puntos del programa. También se presentan algunas
ideas sobre cómo dar forma a la presentación de sus resultados.
6
#include <iostream>
#include <iomanip>
/∗
∗ Pre: r >= 0.0
∗ Post: Escribe por pantalla , en una misma lı́nea, el valor del radio <r> y de la
∗ longitud de una circunferencia de radio <r>
∗/
void datosCircunferencia (double r) {
// Define el valor de la constante trigonométrica PI
const double PI = 3.1416;
// Presenta los valores del radio y la longitud de la circunferencia
cout << fixed << setprecision(2) << setw(7) << r << setprecision(3)
<< setw(16) << 2.0 ∗ PI ∗ r << endl;
}
/∗
∗ Pre: −−−
∗ Post: Escribe por pantalla el radio y la longitud de tres circunferencias
∗/
int main() {
// Escribe el encabezamiento del listado de resultados
cout << setw(7) << ”Radio” << setw(20) << ”Circunferencia” << endl;
cout << setw(7) << ”=====” << setw(20) << ”==============” << endl;
// Escribe el radio y la longitud de tres circunferencias
datosCircunferencia(1.234);
datosCircunferencia(5.0112);
datosCircunferencia(11.5178);
// El programa termina normalmente devolviendo un valor 0
return 0;
}
7
circunferencias.
Podemos invocar la ejecución del programa. Éste presenta por pantalla los resultados que se
muestran a continuación.
computador $ ./circunferencias
Radio Circunferencia
===== ==============
1.23 7.753
5.01 31.486
11.52 72.369
computador $ ...
Cuando se ejecuta cualquiera de los programas anteriores los resultados se presentan por la
pantalla del computador y son siempre los mismos. Vamos a diseñar un nuevo programa en el
cual los resultados presentados por el programa difieren en cada ejecución ya que dependen de
los datos que suministre en cada ocasión el operador o usuario del programa.
Diremos que un programa es interactivo cuando dialoga con el usuario que lo ejecuta. El
programa pregunta al usuario por el valor de determinados datos y éste le responde facilitando
esos datos a través del teclado.
El programa mostrado a continuación pide al usuario que defina el radio de un cı́rculo antes
de mostrar por pantalla cuál es el área de dicho cı́rculo.
#include <iostream>
#include <iomanip>
/∗
∗ Pre: r >= 0.0
∗ Post: Escribe por pantalla en una lı́nea el valor del radio y del área de un
∗ cı́rculo de radio <r>
∗/
void circulo (double r) {
// Define el valor de la constante trigonométrica PI
const double PI = 3.1415926;
// Presenta en una lı́nea los valores del radio y el área del cı́rculo
cout << ”El area de un circulo de radio ” << fixed << setprecision(2) << r
<< ” es igual a ” << PI ∗ r ∗ r << endl;
}
8
/∗
∗ Pre: −−−
∗ Post: Pregunta al operador por el ”Radio del circulo : ” y le informa en la
∗ lı́nea siguiente del valor del radio y del área del cı́rculo
∗/
int main() {
// Pregunta al operador por el radio de un cı́rculo
cout << ”Radio del circulo: ” << flush;
// Lee la respuesta y la almacena en <r>
double r;
cin >> r;
// Presenta por pantalla los datos del cı́rculo de radio r
circulo (r );
// Concluye normalmente y devuelve un 0
return 0;
}
En la función main() pregunta al operador por el Radio del circulo: sin concluir la lı́nea.
Ello se logra mediante la función flush que provoca la presentación por pantalla de la secuencia
de caracteres enviados hacia ella antes de completar la lı́nea.
Le sigue la instrucción cin >> r; que asigna a la variable real r de tipo double el valor
numérico escrito por el usuario como respuesta a la pregunta anterior. El objeto predefinido cin
está asociado, por defecto, al teclado del terminal del operador.
Podemos invocar la ejecución del programa circulo que mantiene el siguiente diálogo con el
usuario. La respuesta del usuario se ha subrayado y escrito con letra negrita. Puede sorprender
la discrepancia entre el valor del radio deterrminado por el operador y el escrito por el programa.
La explicación es muy simple; basta comprender que el programa escribe el valor del radio con
solo dos decimales, tras proceder al redondeo de su valor.
computador $ ./circulo
Radio del circulo: 23.0754
El área de un cı́rculo de radio 23.08 es igual a 1672.82
computador $ ...
Si el dato del radio facilitado por el usuario es diferente al anterior, los resultados calculados
el programa serán, por lógica, distintos.
9
computador $ ./circulo
Radio del circulo: 2.6
El area de un circulo de radio 2.60 es igual a 21.24
computador $ ...
Una función puede tener asociados cero, uno o más parámetros. Un parámetro es un dato
por el cual se transmite información a la función al ser invocada (un dato de entrada) o, como
veremos en un capı́tulo posterior, puede ser un dato que permite transmitir resultados calculados
por la funció invocada (un dato de salida. Una función puede devolver un resultado mediante
la orden return, la última que ejecuta la función. El tipo del resultado devuelto se declara en la
cabecera de la función. El resultado devuelto es un dato de salida de la función. En la cabecera
de una función que no devuelva ningún resultado debe constar la palabra reservada void.
Una función se programa para que pueda ser invocada cuantas veces sea preciso. Para
programar correctamente la invocación de una función y facilitar una adecuada utilización de
su resultado, conviene que el código de la función haya sido documentado mediante una clara y
precisa especificación.
Precondición. La precondición describe las condiciones que deben satisfacer los datos de
entrada de la función al ser ésta invocada. Por el momento consideraremos como datos
de entrada de una función exclusivamente a sus parámetros. Escribiremos la precondición
tras la abreviatura Pre.
Postcondición. La postcondición describe las condiciones que deben satisfacer, en su
caso, sus datos de salida y el resultado devuelto por la función ası́ como los restantes
efectos producidos por su ejecución. Por ejemplo, la modificación de los valores de datos
externos a ella a los que tenga acceso o los efectos producidos en la pantalla (escritura de
resultados en ellas). Escribiremos la postcondición tras la abreviatura Post.
/∗
∗ Pre: −−−
∗ Post: Devuelve el valor del polinomio a ∗ xˆ2 + b ∗ x + c
∗/
10
double calcular (double a, double b, double c, double x) {
return (( a ∗ x + b) ∗ x) + c;
}
/∗
∗ Pre: −−−
∗ Post: Escribe por pantalla una lı́nea con la fecha definida por dia, mes y anyo
∗ con el siguiente formato: dia/mes/anyo. Por ejemplo:
∗ 12/10/1492
∗/
void escribirFecha (int dia, int mes, int anyo) {
cout << dia << ”/” << mes << ”/” << anyo << endl;
}
/∗
∗ Pre: −−−
∗ Post: Presenta por pantalla una lı́nea con el texto ”En esta asignatura se aprende a programar”
∗/
void anunciar () {
cout << ”En esta asignatura se aprende a programar” << endl;
}
La función factorial(n) devuelve un valor entero, concretamente el valor del factorial del
número natural n. Recordemos que el factorial del número natural n se denota matemáticamente
de la forma n! y su valor se define mediante las siguientes relaciones:
n = 0 → n! = 1
n > 0 → n! = 1 × 2 × · · · × n
/∗
∗ Pre: n >= 0
∗ Post: Devuelve el valor de n!
∗/
int factorial (int n)
Todas y cada una de las funciones que se escriban en este curso de programación y en los
trabajos derivados de él, irán precedidas por su correspondiente especificación, en la que debe
constar su precondición y su postcondición.
11
1.4. Propiedades de un algoritmo y guı́a de estilo
Su corrección por la que un algoritmo debe proporcionar una solución correcta del
problema planteado, es decir, los resultados han de ser conformes a su especificación y
deben resolver el problema.
Su legibilidad por la que el documento que describe el algoritmo ha de ser fácil de entender
no solo por su programador, sino por otras personas que posean unos conocimientos
suficientes de programación (por ejemplo, por otros programadores).
En este curso programación seremos fieles a una guı́a de estilo de programación en C++.
Los ejemplos que se presentan en este texto y los que se presentarán en clase seguirán las reglas
de dicha guı́a. Del mismo modo los problemas de programación que resuelvan los alumnos del
curso y los programas que desarrollen en sus prácticas, trabajos y exámenes también deberán
ser fieles a dicha guı́a.
12
Capı́tulo 2
Lenguajes de programación y
ejecución de un programa
A lo largo de los estudios de Ingenierı́a Informática existen varias asignaturas para estudiar en
profundidad lo relativo a lenguajes de programación, compiladores e intérpretes, computadores
y sistemas operativos.
No obstante, una persona que inicia su formación como programador debe tener, desde el
principio, ciertas nociones sobre los conceptos más básicos relacionados con la programación de
computadores. Este capı́tulo pretende anticipar una parte de esta formación básica. Para ello se
hace una presentación simplificada y resumida de los principales conceptos sobre lenguajes de
programación, compiladores e intérpretes, funcionamiento de un computador y funcionamiento
de un sistema operativo.
Estas nociones serán reforzadas en las clases y en las sesiones de prácticas de la asignatura.
En este primer curso de programación vamos a utilizar C++ como lenguaje de referencia,
aunque cualquier otro lenguaje de propósito general de los anteriormente citados hubiera
constituido una opción igualmente válida.
Buena parte de los elementos que se presentan al describir el lenguaje C++ están presentes y
son generalizables a los restantes lenguajes de programación antes citados.
El lenguaje C++ fue diseñado a mediados de los años 1980 por Bjarne Stroustrup en los
13
Laboratorios Bell de la compañı́a AT&T. Constituye una extensión del lenguaje de programación
C, creado en 1972 por Dennis M. Ritchie, también en los Laboratorios Bell. C++ permite diseñar
programas imperativos estructurados e incorpora la posibilidad de realizar una programación
orientada a objetos. En 1998 se publica un estándar del lenguaje C++, el ISO/IEC 14882, revisado
posteriormente en 2011.
Identificadores: son nombres que se asocian a los elementos que el programador define
en los programas que escribe (datos constantes, datos variables, funciones, clases, etc.): x,
i, cuenta, nombrePersona, DIMENSION, buscar, ordenar, etc. Sirven para nombrar dichos
elementos cuando son definidos y cuando son utilizados en el programa.
Un identificador en C++ se construye con una secuencia de caracteres con las siguientes
caracterı́sticas:
• Un identificador que represente el nombre de una función C++ comenzará por una
letra minúscula . Ejemplos: toString, calcular, sumar, buscar
• Un identificador que represente el nombre de un dato variable comenzará por una
letra minúscula . Ejemplos: contador, i, x25, sumaDatos
• Un identificador que represente el nombre de un dato constante se escribirá, todo él,
con letras mayúsculas. Ejemplos: PI, MAXIMO, DIMENSION, AVISO
Palabras clave o palabras reservadas del lenguaje. Son identificadores que tienen un
significado y una función especı́fica dentro de cada lenguaje y, por lo tanto, están reservadas
exclusivamente para ello. En C++ son las siguientes:
14
asm auto bool break case
catch char class const const_cast
continue default delete do double
dynamic_cast else enum explicit extern
false float for friend goto
if inline int long mutable
namespace new operator private protected
public register reinterpret_cast return short
signed sizeof static static_cast struct
switch template this throw true
try typedef typeid typename union
unsigned using virtual void volatile
while
En C++ existen otros operadores cuya utilidad se irá poniendo de manifiesto en capı́tulos
posteriores.
Otros operadores
nombre del operador operador sintaxis
Llamada o invocación a función () f (. . . )
Indexación en vector o tabla [] a[. . . ]
Indirección o desreferencia ∗ ∗a
Dirección o referencia & &p
Conversión de tipo tipo() tipo (a)
Tamaño de sizeof ( ) sizeof ( ) a o sizeof (tipo)
Resolución de ámbito :: a :: b
15
Sı́mbolos sepradores, finalizadores y delimitadores
Tipos de sı́mbolos caracteres que los definen
Carácter en blanco
Caracteres separadores Carácter tabulador
Carácter de nueva lı́nea
Separador: , Carácter coma (,)
Finalizador: ; Carácter punto y coma (;)
Carácter de apertura de paréntesis: (
Delimitador par de paréntesis: (. . . )
Carácter de cierre de paréntesis: )
Carácter de apertura de corchetes: [
Delimitador par de corchetes: [. . . ]
Carácter de cierre de corchetes: ]
Carácter de apertura de llaves: {
Delimitador par de llaves: {. . . }
Carácter de cierre de llaves: }
16
2.1.2. La notación de Backus-Naur o BNF y la construcción de identificadores
La notación BNF emplea los siguientes meta-sı́mbolos para definir reglas sintácticas:
Se van a presentar algunas reglas sintácticas del lenguaje C++ como primera ilustración de la
utilidad de la notación BNF.
La regla anterior expresa que un sı́mbolo identificador se construye como una secuencia
de caracteres tal que el primero es un carácter que pertenece a las categorı́as mayúscula o
minúscula o se trata del carácter "_" y cada uno de los caracteres que, en su caso, le siguen
pueden pertenecer a las categorı́as mayúscula, minúscula o dı́gito o ser el carácter "_". Las
definiciones de las categorı́as mayúscula, minúscula y digito se presentan a continuación
mediante sus correspondientes reglas sintácticas. C++ distingue entre letras mayúsculas y
minúsculas, tal como se ha mencionado anteriormente.
<mayúscula> ::= "A" | "B" | "C" | "D" | "E" | "F" | "G" | "H" | "I" |
"J" | "K" | "L" | "M" | "N" | "O" | "P" | "Q" | "R" |
"S" | "T" | "U" | "V" | "W" | "X" | "Y" | "Z" |
<minúscula> ::= "a" | "b" | "c" | "d" | "e" | "f" | "g" | "h" | "i" |
"j" | "k" | "l" | "m" | "n" | "o" | "p" | "q" | "r" |
"s" | "t" | "u" | "v" | "w" | "x" | "y" | "z" |
<digito> ::= "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9"
17
De acuerdo con las reglas anteriores, los siguientes identificadores son correctos:
La sintaxis de un lenguaje viene definida por el conjunto de reglas que determinan cómo
encadenar sı́mbolos para formar frases correctas. Un primer ejemplo ilustrativo lo constituye la
regla sintáctica correspondiente a un bloque de código C++.
En la regla anterior se utilizan los metacaracteres llaves. Lo que encierran un par de llaves
puede aparecer cero, una o más veces en la frase descrita. La regla establece que un bloque de
código se describe como una secuencia integrada por cero o más declaraciones seguidas por cero
o más instrucciones. Ambas secuencias vienen precedidas por el sı́mbolo de apertura de llaves,
{, y deben estar seguidas por el sı́mbolo de cierre de llaves, }. Un par de ejemplos de bloques
de código C++ se presentan a continuación.
El primero de ellos permuta los valores de dos variables, uno y otro. Para ello se hace uso
de la variable aux declarada en el propio bloque.
{
int aux = uno;
uno = otro;
otro = aux;
}
18
{
i = i + 1;
f = i ∗ f;
}
Por lo tanto, la regla sintáctica anterior expresa que una instrucción condicional se construye
como una secuencia que comienza con el sı́mbolo if seguido por una condición, escrita entre
paréntesis, seguida por una instrucción o por un bloque de instrucciones. Seguidamente, y de
forma opcional, la instrucción puede incorporar cero o más cláusulas else if. La instrucción
finaliza con una o ninguna cláusula else.
if (x > 0) {
y = x ∗ y;
}
else {
y = x + y;
}
if (x > y) {
z = x;
}
else {
z = y;
}
if (x < 0) {
x = −x ;
}
if (x < y) {
19
z = x;
x = y;
y = z;
}
20
if (c == ’S’) {
x += y;
} else if (c == ’R’) {
x −= y;
}
else if (c == ’M’) {
x ∗= y;
}
else if (c == ’D’) {
x /= y;
}
else {
x = 0;
}
if (c == ’S’) {
x += y;
}
else if (c == ’R’) {
x −= y;
}
else if (c == ’M’) {
x ∗= y;
}
else if (c == ’D’) {
x /= y;
}
La escritura en una o en varias lı́neas de una secuencia de sı́mbolos es, en principio, una
prerrogativa del programador. Su decisión debe estar siempre orientada a facilitar la lectura
del código. De igual forma, el programador es libre de añadir cuantos espacios en blanco
considere oportunos, como separadores entre dos sı́mbolos consecutivos. Las guı́as de estilo
incluyen recomendaciones sobre cómo escribir este tipo de secuenciaa de sı́mbolos.
Si consideramos la instrucción:
if (x > 0) {
y = y * x;
}
else {
y = y + x;
}
21
La semántica de esta instrucción condicional determina que se evalúe en primer lugar la
condición x>0. Si el resultado es un valor lógico cierto entonces se procede a ejecutar el bloque
que contiene la instrucción y = y * x;. En caso contrario se ejecuta el bloque con la instrucción
y = y + x; asociado a la cláusula else.
La ejecución interpretada de un programa fuente puede ser mucho más ineficiente que
la ejecución de un programa ejecutable (compilado) que ha sido previamente compilado.
Por ello, un programa que deba ser ejecutado frecuentemente, debiera haber sido compilado
previamente. La interpretación es útil para la puesta a punto de un programa o para programas
que requieren ser ejecutados un número muy reducido de veces.
22
1. Se procede, en primera instancia, a compilar el programa fuente para obtener un
programa objeto equivalente expresado en el código binario del computador en el
que se está trabajando.
1. Se procede, en primera instancia, a compilar una sola vez el programa fuente para obtener
un programa objeto equivalente expresado en un código de nivel intermedio entre
el lenguaje fuente y el código máquina, el denominado código de la Máquina Virtual
Java (JVM ).
Memoria en la que se almacenan los programas y los datos tratados por éstos.
23
Unidad central de proceso (CPU) cuya función es ejecutar las instrucciones descritas
en los programas. Estas acciones pueden llevar aparejados cálculos que se ejecutan en su
unidad aritmético-lógica (ALU).
Gestión de la memoria
Control de dispositivos periféricos
Acceso a ficheros
Asignación de recursos y ordenación de tareas (scheduling)
Etc.
24
Capı́tulo 3
Un tipo de dato define un patrón para representar información y para trabajar con ella.
Cada tipo de dato tiene asociado:
En el lenguaje C++ se definen los cuatro tipos de datos elementales que se resumen en la
siguiente tabla.
25
El lenguaje C++ define variantes de algunos de los tipos anteriores hasta completar la definición
de veinte tipos de datos elementales diferentes. En este curso nos limitaremos a trabajar con los
cuatro tipos de datos elementales presentados en la tabla anterior.
C++ da libertad para que en cada implementación se decida la cantidad de memoria dedicada
a representar cada uno de los datos de un mismo tipo. La función sizeof(T) proporciona como
resultado el número de bytes (un byte corresponde a 8 bits o cifras binarias) destinados a
representar internamente cada dato del tipo T. Por otra parte cada tipo de dato es representado
internamente utilizando un determinado sistema binario de codificación.
Como resultado de las decisiones anteriores, queda definido el dominio de valores de cada
tipo que pueden ser representados internamente mediante su correspondiente código binario.
Los operadores de relación permiten comparar un par de datos del mismo tipo y
proporcionan como resultado un valor lógico o booleano, es decir, un valor cierto (true) o
falso (false).
26
Operadores binarios de relación o comparación
nombre del operador operador sintaxis tipo del resultado
Menor que < a<b bool
Menor o igual que <= a <= b bool
Mayor que > a>b bool
Mayor o igual que >= a >= b bool
Igual que == a == b bool
No igual que != a! = b bool
Operadores aritméticos
nombre del operador operador sintaxis resultado
Signo más + +a el valor a
Signo menos − −a el valor -a
Suma + a+b la suma de a y b
Resta − a−b la diferencia de a menos b
Multiplicación ∗ a∗b el producto de a por b
División / a/b el cociente, entero o real, de a dividido por b
Módulo (resto) % a %b el resto entero de a dividido por b
Pre-incremento ++ ++a incrementa el valor de a, antes de usarlo
Post-incremento ++ a++ incrementa el valor de a, después de usarlo
Pre-decremento -- --a decrementa el valor de a, antes de usarlo
Post-decremento -- a-- decrementa el valor de a,después de usar
Conviene tener presente que el resultado de la división entre dos números enteros es su
cociente entero por defecto (ejs.: 27/5 = 5 y 3/8 = 0), mientras que la divisón entre dos números
reales es su cociente real (ej.: 27. 0/5. 0 = 5. 4 y 3. 0/8. 0 = 0. 375).
La operación módulo sólo se puede plantear sobre un par de números enteros. Su resultado
es el resto de su divisón entera (ej.: 27 %5 = 2 y 3 %8 = 3).
Los operadores lógicos o booleanos permiten programar operaciones entre valores de tipo
bool, obteniendo como resultado también un valor de tipo bool.
Las tablas de verdad presentadas a continuación definen el resultado de cada operación lógica
o booleana, en función de los valores de sus operandos.
27
Tablas de verdad de los operadores lógicos o booleanos
Operando Negación lógica
a !a
true false
false true
Operando 1o Operando 2o Conjunción lógica Disyunción lógica
a b a && b a || b
true true true true
true false false true
false true false true
false false false false
Operadores de asignación
nombre del operador operador sintaxis valor asignado a la variable
Asignación = v = expresión expresión
Suma y asignación += v + = expresión v + expresión
Resta y asignación −= v − = expresión v − expresión
Multiplicación y asignación ∗= v ∗ = expresión v ∗ expresión
División y asignación /= v / = expresión v/expresión
Módulo y asignación %= v % = expresión v %expresión
A continuación se presenta una tabla con los operadores predefinidos en C++ ordenados
según su prioridad, de mayor a menor prioridad. La tabla incluye una columna encabezada
por asociatividad, que indica el orden en que deben ser evaluados dos operadores con un
mismo nivel de prioridad en caso de entrar en conflicto.
28
Clasificación de los operadores predefinidos en C++ por nivel de prioridad
Nivel Operadores Descripción Asociatividad
1 :: resolución de ámbito de izda a dcha
++ -- post-incremento y post-decremento de izda a dcha
() llamada o invocación a función de izda a dcha
[] elemento de vector o tabla de izda a dcha
. selección de elemento por referencia de izda a dcha
2 -> selección de elemento con puntero de izda a dcha
const cast conversión de tipo de izda a dcha
dynamic cast conversión de tipo de izda a dcha
reinterpret cast conversión de tipo de izda a dcha
static cast conversión de tipo de izda a dcha
++ -- pre-incremento y pre-decremento de dcha a izda
+ - signo positivo y negativo de dcha a izda
! ∼ negación lógica y negación bit a bit de dcha a izda
tipo() conversión de tipo de dcha a izda
3 * indirección de dcha a izda
& dirección de de dcha a izda
sizeof tamaño de de dcha a izda
new new[] asignación dinámica de memoria de dcha a izda
delete delete[] desasignación dinámica de memoria de dcha a izda
4 .* ->* selección de campo desde puntero de izda a dcha
* multiplicación de izda a dcha
5 / división de izda a dcha
% módulo (resto de la división entera) de izda a dcha
+ suma de izda a dcha
6
- resta de izda a dcha
7 << >> operadores de desplazamiento de bits de izda a dcha
8 < <= > >= operadores de relación de izda a dcha
9 == ! = operaciones de relación de izda a dcha
10 & AND bit a bit de izda a dcha
11 ^ XOR bit a bit de izda a dcha
12 | OR bit a bit de izda a dcha
13 && AND lógico o conjunción lógica de izda a dcha
14 || OR lógico o disyuncion lógica de izda a dcha
15 c? t : f operador ternario de dcha a izda
= asignación de dcha a izda
+= −= suma o resta y asignación de dcha a izda
16 ∗ = \= %= producto, división o módulo y asignación de dcha a izda
<<= >>= desplazamiento de bits y asignación de dcha a izda
&= ^= |= operaciones bit a bit y asignación de dcha a izda
17 throw lanzamiento de excepción
18 , separación de expresiones de izda a dcha
29
Una expresión se evalúa ejecutando las operaciones asociadas a cada uno de los operadores
que intervienen en ella. El orden en que deben ser ejecutadas, en ausencia de paréntesis, viene
dictado por dos reglas:
1. En primer lugar, atendiendo al nivel de prioridad de cada operador. Ası́, por ejemplo, en
la expresión:
x = a + b ∗ c <= h
Los cuatro operadores que en ella intervienen, =, +, ∗ y <= , tienen diferente nivel
de prioridad, Por ello, el orden de evaluación de las cuatro operaciones descritas en ella
responde a dicho nivel y es el siguiente: 1o ) la multiplicación (nivel 5): [b ∗ c], 2o ) la suma
(nivel 6): [a + b ∗ c], 3o ) la comparación (nivel 8): [a + b ∗ c <= h] y 4o ) la asignación
(nivel 16): [x = a + b ∗ c <= h]
2. En caso de plantearse un conflicto entre operadores del mismo nivel jerárquico, el orden de
aplicación de los operadores es el establecido en la columna derecha de la tabla anterior.
Ası́, por ejemplo, en la expresión:
a %b ∗ c/d
Los tres operadores que en ella intervienen, %, ∗ y /, tienen igual nivel de prioridad (nivel
5) y, por lo tanto, han de ser evaluados según su asociatividad, es decir, de izquierda a
derecha. Por ello, el orden de evaluación de las tres operaciones descritas en ella es el
siguiente: 1o ) el módulo: a %b, 2o ) la multiplicación: a %b ∗ c y 3o ) la división: a %b ∗ c/d
El orden de evaluación de los operadores que intervienen en una expresión puede ser
modificado mediante el uso de paréntesis. Por ejemplo:
x = (a + b) ∗ c <= h
a %(b ∗ (c/d))
Una variable es un dato de un tipo determinado que tiene asociado un valor, el cual puede
ser modificado cuantas veces se desee. Por el momento, únicamente se va a trabajar con variables
estáticas que han de ser declaradas asociándoles un nombre y un tipo de dato y, opcionalmente,
asignándoles un valor inicial. El nombre ha de ser un identificador válido. En caso de no tener
asignado un valor inicial, el valor de la variable se considera indefinido, es decir, aunque tiene
asociado un valor no se puede hacer ninguna hipótesis sobre su valor inicial.
/∗
∗ Ejemplos de declaraciones de variables cuyo valor inicial está indefinido
∗/
int i , j , k; // Tres variables enteras de tipo [ int ] cuyos valores están indefinidos
char c1, c2; // Dos variables de tipo [char] cuyos valores están indefinidos
bool b; // Variable de tipo [ bool ] cuyo valor está indefinido
double r1, r2, r3; // Tres variables reales de tipo [double] cuyos valores están indefinidos
30
En las declaraciones de variables que se presentan a continuación, algunas de las variables
tienen definido su valor inicial (x, z, c, d, v1, v2, v3, b1, b2 y b3) mientras que las restantes
variables (w, e, f, y b4) tienen un valor inicial indefinido.
/∗
∗ Más declaraciones de variables . Algunas de ellas tiene definido su valor inicial
∗ mientras que otras tienen su valor inicial indefinido
∗/
double x = 2.17, w, z = 0.17E−12; // Solo los valores iniciales de x y z están definidos
char c = ’H’, d = ’; ’ , e, f ; // Solo los valores iniciales de c y d están definidos
int v1 = 1, v2 = 0, v3 = 1000; // Los valores iniciales de las tres variables están definidos
bool b1 = true; // El valor inicial de b1 está definido
bool b2 = true, b3 = false, b4; // Solo los valores iniciales de b2 y b3 están definidos
/∗
∗ Ejemplos de constantes de diversos tipos
∗/
const double PI = 3.1416; // Constante trigonométrica PI
const char PRIMERA = ’A’, ULTIMA = ’Z’; // Letras primera y última del alfabeto
const int DIAS SEMANA = 7; // Número de dı́as de la semana
const int NUMERO MESES = 12; // Número de meses del año
const bool CIERTO = true, FALSO = false; // Los dos valores booleanos
Se aconseja escribir los identificadores asociados a constantes con letras mayúsculas para
facilitar su localización en los puntos del código en los que son utilizadas.
Asociar a una constante un simbolo (PI, PRIMERA, ULTIMA, DIAS SEMANA, etc) tiene grandes
ventajas. Por una parte, el código resulta más legible y, por otra, es mucho más fácil modificar
el valor de una constante.
/∗
∗ Ejemplos de constantes de diversos tipos
∗/
const double PI = 3.14159265; // Constante trigonométrica PI
const char PRIMERA = ’A’, ULTIMA = ’Z’; // Letras primera y última del alfabeto
const int DIAS SEMANA = 7; // Número de dı́as de la semana
const int NUMERO MESES = 12; // Número de meses del año
const bool CIERTO = true, FALSO = false; // Los dos valores booleanos
De no haber procedido a trabajar con esta definición simbólica de PI, el valor 3. 1416 se
31
repetirı́a en 100 puntos del programa y serı́a necesario sustituirlo por 3. 14159265 en cada punto,
con el riesgo de cometer algún error o de omitir alguna de las 100 modificaciones.
El operador de asignación = permite asignar un nuevo valor a una variable (por ejemplo, a
la variable v), el resultante de evaluar una expresión (por ejemplo, expresión). El nuevo valor
asignado a la variable v sustituye su valor previo.
v = expresión;
2. En el caso de que el resultado obtenido al evaluar expresión no fuera del mismo tipo que
la variable v se hace una conversión implı́cita del resultado al tipo de v.
/∗
∗ En cada una de las instrucciones de asignación se calcula una de las magnitudes geométricas
∗ (áreas y volumen) de un cilindro recto de altura ”h” y base circular de radio ”r”
∗/
const double PI = 3.14159265; // Constante PI
double r = 12.5; // Radio de la base del cilindro
double h = 120.0; // Altura del cilindro
double areaBase = PI ∗ r ∗ r; // Area de la base del cilindro
double areaLateral = 2.0 ∗ PI ∗ r ∗ h; // Area lateral del cilindro
double areaTotal = 2.0 ∗ areaBase + areaLateral; // Area total del cilindro
double volumen = areaBase ∗ h; // Volumen del cilindro
/∗
∗ Operaciones de adicionales de asignación, precedidas por una operación binaria cuyo primer
32
∗ operando es el valor de partida de la variable a la que se va a asignar un nuevo valor
∗/
variable += expresión; // Equivale a: variable = variable + expresión;
variable −= expresión; // Equivale a: variable = variable − expresión;
variable ∗= expresión; // Equivale a: variable = variable ∗ expresión;
variable /= expresión; // Equivale a: variable = variable / expresión;
variable %= expresión; // Equivale a: variable = variable % expresión;
/∗
∗ Ilustración del comportamiento de los operadores de pre−incremento, post−incremento,
∗ pre−decremento y post−decremento
∗/
int v, i ; // v = indefinido e i = indefinido
i = 100; // v = indefinido e i = 100
v = i++; // v = 100 e i = 101
v = ++i; // v = 102 e i = 102
v = i−−; // v = 102 e i = 101
v = −−i; // v = 100 e i = 100
En los apartados que siguen se explican ambas formas de conversión de tipos de datos.
/∗
∗ Ejemplo que ilustra la conversión implı́cita de datos de tipo double en datos de tipo int .
∗ La conversión se hace mediante truncamiento. Se puede transformar en un redondeo si el
∗ valor de tipo double se incrementa en 0.5 unidades, antes de ser truncado
∗/
double f = 17.575; // El valor de f es igual a 17.575
int i ; // El valor de i está indefinido
33
i = f; // El valor de i es ahora igual a 17 (valor truncado)
i = f + 0.5; // El valor de i es ahora igual a 18 (valor redondeado)
/∗
∗ Primer ejemplo de conversiones implı́citas de tipos
∗/
int base = 5; // El valor de la base del rectángulo es igual a 5
int altura = 3; // El valor de la altura del rectángulo es igual a 3
int areaEntera = base ∗ altura; // Se asigna a areaEntera el valor 15 de tipo int
double areaReal = base ∗ altura; // Se asigna a areaReal el valor 15.0 de tipo double
En el siguiente ejemplo, la base del rectángulo sigue siendo igual a 5 unidades (una magnitud
entera de tipo int), mientas que la altura del rectángulo es igual a 3.15 unidades (una magnitud
real de tipo double). La expresión que calcula el área del rectángulo es el producto base *
altura, cuyos operandos son datos de dos tipo distintos, int y double. Ello exige la conversión
implı́cita previa del dato menos general, el dato de tipo int, en un dato equivalente del tipo más
general, un dato de tipo double. Como consecuencia, el resultado del producto es el valor 15.75
de tipo double. La asignación de este valor a la variable areaReal se realiza sin problemas,
mientras que su asignación directa a la variable areaEntera no es posible, y se requiere la
conversión implı́cita previa del valor 15.75 de tipo double a un valor de tipo int. Como
la conversión en un valor equivalente no es posible, el lenguaje C++ optar por convertirlo en
el entero resultante de truncar el valor real, es decir, de eliminar de él sus decimales. Como
consecuencia, el valor asignado a la variable areaEntera es el valor 15 de tipo int.
/∗
∗ Segundo ejemplo de conversiones implı́citas de tipos
∗/
int base = 5; // El valor de la base del rectángulo es igual a 5
double altura = 3.15; // El valor de la altura del rectángulo es igual a 3.15
int areaEntera = base ∗ altura; // Se asigna a areaEntera el valor 15 de tipo int
double areaReal = base ∗ altura; // Se asigna a areaReal el valor 15.75 de tipo double
34
/∗
∗ Ejemplo de conversión explı́cita de datos de tipo int en datos equivalentes de tipo double
∗/
int a = 3, b = 7; // El valor de a es igual a 3 y el de b igual a 7
double f; // El valor de f está indefinido
f = a / b; // El valor de f es ahora igual a 0.0
f = double(a / b); // El valor de f es ahora igual a 0.0
f = double(a) / b; // El valor de f es ahora igual a 0.42871
f = a / double(b); // El valor de f es ahora igual a 0.42871
f = double(a) / double(b); // El valor de f es ahora igual a 0.42871
La utilización de este operador puede ser conveniente en problemas como el que se ilustra a
continuación. El cálculo del porcentaje de aprobados en una asignatura requiere la conversión
previa de los datos enteros que representan el número de alumnos aprobados y el de alumnos
matriculados a datos de tipo double.
/∗
∗ Alumnos matriculados y alumnos aprobados en una asignatura
∗/
int matriculados = 160; // Número de alumnos matriculados
int aprobados = 95; // Número de alumnos aprobados
/∗
∗ La variable porcentaje representará el porcentaje de alumnos aprobados
∗/
double porcentaje;
/∗
∗ Cuatro alternativas para el cálculo del valor de porcentaje . Son válidas únicamente
∗ la segunda y la tercera
∗/
porcentaje = 100 ∗ (aprobados / matriculados);
// El primer valor calculado de porcentaje es igual a 0.0 [ este resultado no es válido ]
porcentaje = 100.0 ∗ (double(aprobados) / double(matriculados));
// Este nuevo valor de porcentaje es igual a 59.375 [ este resultado sı́ es válido ]
porcentaje = 100 ∗ (double(aprobados) / matriculados);
// El tercer valor de porcentaje también es igual a 59.375 [ este resultado sı́ es válido ]
porcentaje = 100 ∗ double(aprobados / matriculados);
// El cuarto valor de porcentaje es igual a 0.0 [ este resultado no es válido ]
El primer cálculo del valor de porcentaje no es válido ya que al dividir los dos datos enteros
que representan los números de aprobados y de matriculados, 95/160, el resultado que se obtiene
es cero, 95/160 = 0.
Los cálculos segundo y tercero son ambos válidos. En el segundo se fuerza de modo explı́cito
la conversión de los valores enteros aprobados y matriculados a datos de tipos double, antes de
dividir 95.0/160.0 cuyo resultado es 95.0/160.0 = 59.375. En el tercero, se fuerza de modo
explı́cito la conversión del valor entero aprobados al tipo double, mientras que la conversión del
valor de matriculados al tipo double se hará de forma implı́cita antes de dividir, obteniendo
el mismo resultado que en el caso anterior, 95.0/160.0 = 59.375
El cuarto cálculo tampoco es váido, ya que la conversión a tipo double se aplica al entero
resultante de dividir 95/160 cuyo resultado es cero, 95/160 = 0.
35
3.3.5. Aplicación al cálculo del cambio entre pesetas y euros
36
Capı́tulo 4
En esta lección se van a diseñar algunos programas elementales con objeto de presentar las
caracterı́sticas básicas de la estructura de un programa C++ y se detallarán los mecanismos de
comunicación de datos entre sus funciones.
Deseamos diseñar un programa capaz de escribir por pantalla las tablas de multiplicar que
le demande interactivamente su operador o usuario. El programa escribirá tantas tablas como
le sean demandadas por el operador. Finalizará su ejecución cuando el operador responda con
un cero a la pregunta que le plantea el programa.
39
4.1.2. Estructura del programa
Al diseñar el programa vamos a distinguir dos tareas diferentes, pero complementarias. Cada
una de ellas va a ser encomendada a una función C++ diferente:
LA TABLA DEL 7
7 x 0 = 0
7 x 1 = 7
7 x 2 = 14
. . .
7 x 8 = 56
7 x 9 = 63
7 x 10 = 70
Aquı́ se presenta el código completo del programa. A continuación se explicará cada uno de
los elementos descritos en él.
40
/∗
∗ Autores: Miguel Angel Latre y Javier Martinez
∗ Ultima revisión: 20 de marzo de 2014
∗ Resumen: Programa interactivo que presenta por pantalla las tablas de multiplicar
∗ seleccionadas por el operador
∗/
#include <iostream>
#include <iomanip>
/∗
∗ Pre: −−−
∗ Post: Presenta por pantalla la tabla de multiplicar del <n>
∗
∗ LA TABLA DEL n
∗ nx 0=0
∗ nx 1=n
∗ n x 2 = ...
∗ ...
∗ n x 9 = ...
∗ n x 10 = ...
∗/
void presentarTabla (int n) {
// Escribe el encabezamiento de la tabla de multiplicar del <n>
cout << endl << ”LA TABLA DEL ” << n << endl;
// Escribe las 11 lineas de la tabla de multiplicar del <n>
for (int i = 0; i <= 10; ++i) {
cout << setw(3) << n << ” x ” << setw(2) << i << ” = ”
<< setw(3) << n ∗ i << endl;
}
}
/∗
∗ Pre: −−−
∗ Post: Pregunta reiteradamente al operador qué tabla de multiplicar desea escribir y la escribe
∗ a continuación, salvo que su respuesta sea un 0, en cuyo caso el programa termina
∗/
int main() {
// Plantea la primera pregunta al operador
cout << ”Que tabla desea escribir (0 para acabar): ” << flush;
// Asigna a <multiplicando> el primer valor entero escrito por el operador
int multiplicando;
cin >> multiplicando;
// Itera hasta que el operador responda con un valor gual a 0
while (multiplicando != 0) {
// Ordena escribir por pantalla la tabla de multiplicar de <multiplicando>
presentarTabla(multiplicando);
// Plantea una nueva pregunta al operador
cout << endl << ”Que tabla desea escribir (0 para acabar): ” << flush;
// Asigna a <multiplicando> el nuevo valor entero escrito por el operador
cin >> multiplicando;
}
// La ejecución del programa termina normalmente
return 0;
}
41
El programa anterior comienza con un comentario en el que constan sus autores, la fecha de
su última revisión y un breve resumen de su finalidad. Estas tres informaciones debieran constar
el principio de cualquier programa.
En las dos directivas #include del programa se especifican los nombres de dos bibliotecas
estándar, iostream y iomanip, para poder hacer uso de algunos de los elementos (objetos
y funciones) en ellas definidos. Como resultado práctico, los elementos definidos en ambas
bibliotecas pueden ser utilizados a partir de ese punto del programa.
• cin: objeto que gestiona el flujo de información recibida a través del dispositivo
estándar de entrada (asociado, por defecto, al teclado del terminal)
• cout: objeto que gestiona el flujo de información enviada al dispositivo estándar de
salida (asociado, por defecto, a la pantalla del terminal)
• flush: función que descarga el buffer o tampón intermedio asociado a un dispositivo
de salida.
• endl: función que inserta un carácter de fin de lı́nea en el flujo dirigido hacia un
dispositivo de salida y descarga el buffer o tampón intermedio asociado a dicho
dispositivo.
• setw(n): función que establece el número de caracteres que debe tener, como mı́nimo,
cada uno de los datos que se inserten en el flujo dirigido hacia el dispositivo de salida.
La declaración using namespace std hace visible el espacio de nombres denominado std,
dentro del cual están incluidos los nombres de todos los objetos, funciones y demás elementos
definidos en las bibliotecas estándar.
Cada uno de estos elementos tiene asociado un ámbito o visibilidad (scope en inglés) y una
duración o vida (lifetime en inglés).
42
Cabe distinguir dos posibles ámbitos de un elemento:
Ámbito local de los elementos han sido definidos dentro de un bloque, es decir, dentro
de una zona de código acotada por un par de llaves. Esos elementos son visibles y pueden
ser utilizados por el programador desde el punto en que han sido definidos hasta el final
de su bloque. El ejemplo que sigue ilustra esta idea:
{
. . .
int i = 10; // A partir de aquı́ es visible i
. . . // Aquı́ solo es visible i
{
double r = 1.47; // A partir de aquı́ es visible r y sigue siéndolo i
. . . // Aquı́ son visibles i y r
} // Fin de la visibilidad de r
. . . // Aquı́ solo es visible i
char c = ’J’; // A partir de aquı́ es visible c y sigue siéndolo i
. . . // Aquı́ son visibles i y c
} // Fin de la visibilidad de i y c
Los parámetros de una función pertenecen al ámbito local del bloque de código asociado
a la función.
Ámbito global: Pertenecen al ámbito global todas las funciones definidas en un fichero
ası́ como los restantes elementos definidos fuera del seno de sus funciones. Los elementos
cuyo ámbito es global son visibles desde el punto del fichero en el que son definidos hasta
el final del propio fichero.
En el ejemplo ilustrativo que se presenta a continuación fechaAmerica es una variable
global. Del mismo modo, mostrarFechas() y main() son funciones cuyo ámbito es global.
Los tres elementos son visibles desde el punto en el que han sido definidos, en el caso de
funciones desde su encabezamiento, hasta el final del fichero.
Dentro del bloque asociado a la función mostrarFechas() son visibles tanto la variable
global fechaAmerica como la variable local fechaLuna.
43
// fechaAmerica
/∗
∗ Ilustra la visibilidad de los datos locales y globales
∗/
void mostrarFechas () { // A partir de aquı́ es visible la función mostrarFechas
int fechaLuna = 1969; // A partir de aquı́ es visible la variable local fechaLuna
cout << ”America fue descubierta en ” << fechaAmerica
<< ” y el hombre llego a la Luna en ” << fechaLuna << endl;
} // Fin de la visibilidad de fechaLuna
/∗
∗ Se limita a invocar la función mostrarFechas()
∗/
int main() { // A partir de aquı́ es visible la función main
mostrarFechas();
return 0;
}
namespace esp1 {
Dado un espacio de nombres, por ejemplo el espacio std, el modo de nombrar a cada uno
de los elementos definidos dentro de él es el resultado de aplicar el operador de ámbito ::
para formar el nombre completo. Ello se logra anteponiento al operador el nombre del espacio
y postponiendo el nombre del elemento. Por ejemplo: std::cout y std::endl.
#include <iostream>
/∗
∗ Pre: −−−
44
∗ Post: Escribe por pantalla el mensaje ”Bienvenidos a la Universidad”
∗/
int main() {
std :: cout << ”Bienvenidos a la Universidad: ” << std::endl;
return 0;
}
Una declaración using namespace std evita tener que preceder el nombre de cada elemento
utilizado del espacio sdt por el nombre del espacio. Por esta razón, se suele incluir dicha
declaración y el codigo del programa queda del modo en que fue también presentado en el
primer capı́tulo de este curso.
#include <iostream>
/∗
∗ Pre: −−−
∗ Post: Escribe por pantalla el mensaje ”Bienvenidos a la Universidad”
∗/
int main() {
cout << ”Bienvenidos a la Universidad” << endl;
return 0;
}
Es importante que el diseñador de cada función tenga máxima libertad para nombrar los
elementos locales con los que trabaja dicha función, es decir, el nombre de sus parámetros, de
sus constantes y variables locales, etc.
Es posible que alguno de los nombres asignados a elementos locales coincida con el de algún
elemento global definido en el mismo fichero o definido en alguna de las bibliotecas incorporadas
la programa mediante una directiva #include. En tal caso el nombre local prevalece sobre el
de ámbito global y enmascara a éste. En caso de querer hacer referencia al elemento global será
necesario hacer uso del operador de ámbito ::, tal como se ilustra a continuación al reescribir
el mismo programa presentado anteriormente.
/∗
∗ Ilustra la visibilidad de los datos locales y globales
∗/
void mostrarFechas () { // A partir de aquı́ es visible la función mostrarFechas
int fecha = 1969; // A partir de aquı́ es visible la variable local fecha
45
cout << ”America fue descubierta en ” << ::fecha // variable global fecha
<< ” y el hombre llego a la Luna en ” << fecha << endl; // variable local fecha
} // Fin de la visibilidad de la variable local fecha
/∗
∗ Se limita a invocar la función mostrarFechas()
∗/
int main() { // A partir de aquı́ es visible la función main()
mostrarFechas();
return 0;
}
Al ser ejecutado el programa anterior, también muestra por pantalla la siguente lı́nea:
Dentro del código de una función, elementos definidos en un bloque interior puede enmascarar
y hacer inaccesibles los elementos definidos en un bloque más exterior, si sus nombres coinciden.
Este problema se ilustra en el ejemplo que sigue. La variable fecha de tipo char definida en el
bloque más interno, enmascara y hace inaccesible desde dicho bloque a las variables fecha de
tipo int definidas en los bloques intermedio y exterior.
{
int fecha = 1969;
{
int fecha = 1492;
{
char fecha = ’?’;
cout << ”La rueda fue inventada en ” << fecha << endl;
}
cout << ”America fue descubierta en ” << fecha << endl;
}
cout << ”El hombre llego a la Luna en ” << fecha << endl;
}
Al ser ejecutado el programa anterior, muestra por pantalla las tres lı́neas siguientes:
46
4.3. ¿Datos globales o parámetros para la comunicación entre
funciones?
Este apartado presenta las pautas a seguir para comunicar información entre las funciones
que integran un programa C++.
Queremos diseñar un programa que mantenga un diálogo con el operador. Éste debe definir
un intervalo de valores enteros y el programa le informa a continuación de la suma de todos los
datos de dicho intervalo.
Escriba los extremos de un intervalo entero [a,b] siendo a<=b: 100 150
Los enteros del intervalo [100,150] suman 6375
Vamos a plantear un diseño modular. El programa estará integrado por tres funciones que
situaremos a tres niveles de abstracción con relación al problema a resolver. El nivel superior será
el más general, es decir, el más alejado de detalles concretos, mientras que en niveles inferiores
se irá profundizando en los detalles del problema a resolver.
Nivel superior. Función main() responsable de dialogar con el operador recabando los
datos que definen un intervalo entero y de ordenar la presentación de los resultados. En este
nivel no se entra a considerar cómo calcular el resultado del problema ni cómo presentarlo.
Nivel inferior. Función sumarDatos() responsable del cálculo del resultado, es decir, de
la suma de todos los datos de un intervalo entero.
En este primer diseño no se define ningún parámetro en ninguna de sus funciones ya que
se ha optado por utilizar dos variables globales, desde y hasta de tipo int, para comunicar
información entre funciones:
La función mostrarResultado() hace uso de los valores de ambas variables globales para
presentar el intervalo de valores enteros por pantalla.
La función sumarDatos() hace uso de los valores de ambas variables globales para calcular
la suma de los datos comprendidos en el intervalo entero que definen.
47
El código completo del programa se presenta a continuación.
#include <iostream>
/∗
∗ Pre: desde <= hasta
∗ Post: Informa por pantalla de la suma de los datos del intervalo entero [desde,hasta]
∗ del siguiente modo, por ejemplo:
∗ Los enteros del intervalo [100,150] suman 6375
∗/
void mostrarResultado () {
cout << ”Los enteros del intervalo [” << desde << ”,” << hasta
<< ”] suman ” << sumarDatos() << endl;
}
/∗
∗ Pre: −−
∗ Post: Pide al operador que defina los extremos de un intervalo entero y ordena presentar
∗ por pantalla el valor de la suma de todos los enteros de dicho intervalo
∗/
int main() {
// El operador debe definir los extremos del intervalo
cout << ”Escriba los extremos de un intervalo entero [a,b] siendo a<=b: ”
<< flush;
cin >> desde >> hasta;
// Presenta por pantalla la suma de los valores del intervalo
mostrarResultado();
// Concuye su ejecución normalmente
return 0;
}
Crı́tica del diseño de este programa. El diseño de este programa es muy desafortunado
por las siguiente razones.
48
No está garantizado que el valor que deben tener los datos sea el definido por el operador.
Supongamos que se añaden funciones al programa para realizar nuevas tareas y estas son
invocadas después de definir los valores de las variables globales desde y hasta. Cualquiera
de estas funciones puede modificar el valor de las variables globales desde y hasta, por
error, desconocimiento o intencionadamente, y arruinar el correcto funcionamiento del
programa.
Un segundo diseño del mismo programa, mucho más afortunado que el anterior, se presenta
a continuación. La comunicación de la información relativa a los extremos del intervalo de datos
enteros a sumar se realiza a través de los parámetros de las funciones sumaDatos(desde,hasta)
y mostrarResultado(principio,fin).
El diseño de las tres funciones es ahora independiente. Puede cambiarse el nombre de las
variables utilizadas para almacenar los valores de los extremos del intervalo en cualquiera de
ellas sin que deba modificarse el código de las restantes funciones. Por las mismas razones, estas
funciones pueden ser reutilizadas sin problemas en el diseño de otros programas.
Por otra parte, no hay ahora riesgo de que se incorpore al programa alguna nueva función
que modifique los valores de los extremos del intervalo de datos enteros a sumar.
#include <iostream>
/∗
∗ Pre: desde <= hasta
∗ Post: Devuelve la suma de los datos comprendidos en el intervalo entero [desde,hasta]
∗/
int sumaDatos (int desde, int hasta) {
return (desde + hasta) ∗ (hasta − desde + 1) / 2;
}
/∗
∗ Pre: principio <= fin
∗ Post: Informa por pantalla de la suma de los datos del intervalo entero [ principio , fin ]
∗ del siguiente modo:
∗ Los enteros del intervalo [100,150] suman 6375
∗/
void mostrarResultado (int principio, int fin ) {
cout << ”Los enteros del intervalo [” << principio << ”,” << fin
<< ”] suman ” << sumaDatos(principio,fin) << endl;
}
49
/∗
∗ Pre: −−
∗ Post: Pide al operador que defina los extremos de un intervalo entero y ordena presentar
∗ por pantalla el valor de la suma de todos los enteros de dicho intervalo
∗/
int main() {
// El operador debe definir los extremos de un intervalo entero
int minimo, maximo;
cout << ”Escriba los extremos de un intervalo entero [a,b] siendo a<=b: ” << flush;
cin >> minimo >> maximo;
// Presenta por pantalla la suma de los valores del intervalo
mostrarResultado(minimo, maximo);
// Concluye normalmente su ejecución
return 0;
}
Cuando es invocada una función, a cada uno de sus parámetros se le transmite un valor.
Por ejemplo, al ejecutarse la invocación mostrarResultado(minimo, maximo) desde la función
main(), al parámetro principio de la función mostrarResultado(principio, fin) se le
asigna el valor resultante de evaluar la expresión minimo y al parámetro fin se le asigna el
valor resultante de evaluar la expresión maximo.
En el caso hipotético de que los valores de los parámetros principio y fin fueran modificado
al ser ejecutado el código de la función mostrarResultado(principio, fin), ello sólo tendrı́a
un efecto local en esa función y en ningún caso podrı́a suponer la modificación del valor de
ningún dato fuera de ella.
Tratemos de diseñar una función que, al ser invocada de la forma que se ilustra a continuación,
permute los valores de dos variables a y b de tipo int.
intercambiar(a, b);
/∗
∗ Pre: uno = X y otro = Y
∗ Post: uno = Y y otro = X
∗/
void intercambiar (int uno, int otro) {
int aux = uno;
uno = otro;
otro = aux;
}
50
ser invocada la función, no recibe los valores de los parámetros, sino sus direcciones en memoria
(referencias). Ello le permite tanto utilizar como modificar el valor de los datos almecenados en
esas direcciones de memoria, es decir, en esas variables.
/∗
∗ Pre: uno = X y otro = Y
∗ Post: uno = Y y otro = X
∗/
void intercambiar (int& uno, int& otro) {
int aux = uno;
uno = otro;
otro = aux;
}
51
/∗
∗ Pre: a = X y b = Y
∗ Post: <a> almacena el menor de los valores {X,Y} y <b> almacena
∗ el mayor de los valores {X,Y}
∗/
void ordenar (int& a, int& b) {
if (a > b) {
intercambiar(a, b);
}
}
/∗
∗ Pre: a = X, b = Y y c = Z
∗ Post: <a> almacena el menor de los valores {X,Y,Z}, <c> almacena el mayor
∗ de los valores {X,Y,Z} y <b> almacena el valor intermedio de {X,Y,Z}
∗/
void ordenar (int& a, int& b, int& c) {
if (a >= b && a >= c) {
intercambiar(a, c );
}
else if (b >= a && b >= c) {
intercambiar(b, c );
}
ordenar(a, b);
}
Los parámetros por referencia se utilizan en funciones que necesitan devolver los valores de
varios datos, como es el caso de la función preguntar(nombre,nacimiento,estatura,peso)
que se presenta a continuación.
/∗
∗ Pre: −−−
∗ Post: Asigna a <nombre>, <nacimiento>, <estatura> y <peso> los valores determinados
∗ por el operador como respuesta a cuatro preguntas que le son formuladas:
∗ Su nombre:
∗ Su anyo de nacimiento:
∗ Su estatura:
∗ Su peso:
∗/
void preguntar (string& nombre, int& nacimiento, double& estatura, double& peso) {
// Pregunta por el nombre y asigna la respuesta del operador a la variable
// <nombre>
cout << ”Su nombre: ” << flush;
cin >> nombre;
// Pregunta por la fecha de nacimiento y asigna la respuesta del operador
// a la variable <nacimiento>
cout << ”Su anyo de nacimiento: ” << flush;
cin >> nacimiento;
// Pregunta por la estatura y asigna la respuesta del operador a la variable
// <estatura>
cout << ”Su estatura: ” << flush;
cin >> estatura;
52
// Pregunta por el peso y asigna la respuesta del operador a la variable <peso>
cout << ”Su peso: ” << flush;
cin >> peso;
}
53
Capı́tulo 5
Instrucciones simples y
estructuradas
En este capı́tulo se presentan las instrucciones simples que C++ pone a disposición del
programador para el tratamiento de información y los mecanismos básicos para estructurar
el código de un programa. Su uso se ilustra en los capı́tulos siguientes.
5.1. Instrucciones
El código de cada una de las funciones de un programa C++ consta de intrucciones que pueden
ser simples o estructuradas.
Los dos subapartados que siguen se dedican a presentar instrucciones simples y estructuradas
de C++.
Entre las instrucciones simples estudiaremos las instrucciones de declaración, las instrucciones
de expresión, las instrucciones de invocación de una función y las instrucciones de devolución
de valor en una función. Entre las instrucciones estructuradas estudiaremos los bloques de
instrucciones, las instrucciones condicionales y dos tipos de intrucciones iterativas.
La sintaxis de las instrucciones simples del lenguaje C++ se detalla a continuación. Conviene
observar que todas ellas concluyen con el sı́mbolo finalizador ”;”.
54
<devoluciónValor> ::= "return" [ <expresión> ]
El repertorio de instrucciones simples del lenguaje C++ se limita a los siguientes tipos
de instrucciones: instruccciones de declaración, instrucciones de expresión, instrucciones de
invocación e intrucciones de devolución de valor que se detallan a continuación.
De todas las posibles expresiones que pueden construirse en C++ sólo tienen interés práctico,
para construir instrucciones, las que se detallan a continuación.
55
• Instrucciones de incremento y decremento que permiten incrementar o
decrementar en una unidad el valor de una variable.
cout << expresión1 << expresión2 << ... << expresiónK; // una instrucción de salida
cout << expresión1 << expresión2 << ... << expresiónK << flush;
56
Y un segundo ejemplo:
cout << expresión1 << expresión2 << ... << expresiónK << endl;
Y un tercer ejemplo:
cout << expresión1 << expresión2 << endl << endl << expresión3 << endl;
En una instrucción de salida se puede hacer uso, entre otros, de los siguientes
manipuladores del flujo de salida:
◦ Manipulador flush: presenta por el dipositivo estándar de salida la secuencia de
caracteres del flujo de salida y vacı́a éste.
◦ Manipulador endl: inserta en el flujo de salida el carácter de nueva lı́nea (’\n’),
presenta por el dipositivo estándar de salida la secuencia de carecteres del flujo
y vacı́a éste.
◦ Manipulador dec: establece que los datos numéricos enteros se representen en
base diez (opción definida por defecto).
◦ Manipulador oct: establece que los datos numéricos enteros se representen en
base ocho.
◦ Manipulador hex: establece que los datos numéricos enteros se representen en
base dieciseis.
◦ Manipulador fixed: establece que los datos numéricos reales se representen en
coma flotante (ej.: 31.486).
◦ Manipulador scientific: establece que los datos numéricos reales se representen
en notación cientı́fica (ej.: 3.1486e+001).
◦ Manipulador left: permite alinear datos a la izquierda.
◦ Manipulador right: permite alinear datos a la derecha.
En la biblioteca estándar iomanip ha sido definida una amplia colección de
manipuladores del flujo adicionales que conviene consultar cuando se desee hacer
presentaciones más sofisticadas de los datos insertos en un flujo de salida.
• Instrucciones de entrada para la lectura de datos textuales a través del dispositivo
estándar de entrada (asociado, por defecto, al teclado del terminal del operador).
cin >> variable1 >> variable2 >> ... >> variableK; // una instrucción de entrada
57
Instrucciones de invocación que consisten en la llamada a una función que no devuelve
ningún valor, es decir, que en su encabezamiento consta void como el tipo de valor a
devolver o a una función cuyo valor devuelto no es utilizado.
58
Bloque secuencial de instrucciones
Las instrucciones del bloque se ejecutan secuencialmente, en el mismo orden en el que han
sido escritas. Un ejemplo de bloque de instrucciones se presenta a continuación:
{
// Intercambia los valores de las variables <a> y <b>, ambas de tipo [int]
int aux = a;
a = b;
b = aux;
}
Para facilitar la legibilidad del código, es muy importante presentar alineadas a izquierda las
lı́neas que describen el código del bloque, ası́ como, sangrar estas lı́neas un cierto número de
carácteres (2, 3 ó 4) respecto a la llave } del final de bloque.
Intrucción condicional
59
Un primer ejemplo de programación de una instrución condicional con cláusula else se
presenta a continuación.
/∗
∗ Asigna a <menor> el menor de los valores de <a> y <b> y
∗ a <mayor> el mayor de los valores de <a> y <b>
∗/
if (a >= b) {
menor = b;
mayor = a;
}
else {
menor = a;
mayor = b;
}
He aquı́ un segundo ejemplo de instrucción condicional con varias cláusulas else if y cláusula
final else.
60
/∗
∗ Asigna a <nombreMes> la referencia a una secuencia de caracteres que representa
∗ el nombre del mes número <numeroMes>, sabiendo que el valor de <numeroMes> está
∗ comprendido entre 1 (enero) y 12 (diciembre).
∗/
string nombreMes;
if (numeroMes == 1) {
nombreMes = ”enero”;
}
else if (numeroMes == 2) {
nombreMes = ”febrero”;
}
else if (numeroMes == 3) {
nombreMes = ”marzo”;
}
else if (numeroMes == 4) {
nombreMes = ”abril”;
}
else if (numeroMes == 5) {
nombreMes = ”mayo”;
}
else if (numeroMes == 6) {
nombreMes = ”junio”;
}
else if (numeroMes == 7) {
nombreMes = ”julio”;
}
else if (numeroMes == 8) {
nombreMes = ”agosto”;
}
else if (numeroMes == 9) {
nombreMes = ”septiembre”;
}
else if (numeroMes == 10) {
nombreMes = ”octubre”;
}
else if (numeroMes == 11) {
nombreMes = ”noviembre”;
}
else {
nombreMes = ”diciembre”;
}
Una instrucción condicional puede no presentar ni cláusula else ni cláusulas else if.
/∗
∗ Si el valor de <a> es un múltiplo de 7 escribe un mensaje informativo, en caso contrario
∗ no ejecuta ninguna acción.
∗/
if (a %7 ==0 ) {
cout << ”El numero ” << a << ” es multiplo de 7” << endl;
}
61
Intrucciones iterativas
/∗
∗ Asigna a <factorial> el valor de n!, siendo n >= 0
∗/
int i = 0,
factorial = 1; // factorial = i! = 0! = 1
while (i != n) {
// factorial = i!
i = i + 1;
factorial = i ∗ factorial ;
// factorial = i!
}
// factorial = n!
62
Como ejemplo se presenta un código C++ construido alrededor de una instrucción iterativa
instrucciónFOR que resuelve un problema idéntico al considerado para ilustrar la instrucción
iterativa anterior.
/∗
∗ Asigna a <factorial> el valor de n!, siendo n >= 0
∗/
int factorial = 1; // factorial = 0! = 1
for (int i = 1; i <= n; ++i) {
// factorial = (i−1)!
factorial = i ∗ factorial ;
// factorial = i!
}
// factorial = n!
63
Capı́tulo 6
El lenguaje C++ ofrece doce tipos de datos diferentes para representar números enteros. Como
se ha mencionado en un capı́tulo previo, en este curso nos limitaremos a hacer uso del tipo int
cuyas caracterı́sticas son las siguientes:
Dedica sizeof(int) para representar internamente un dato entero. Aunque este número
depende de cada implementación, suele ser usual que sizeof(int) = 4 bytes, que
corresponde a 8 bits/byte ×sizeof(int) bytes = 8 bits/byte × 4 bytes = 32 bits.
Utiliza un código binario en complemento a dos de n bits que permite representar los enteros
comprendidos en el intervalo [−2n−1 , 2n−1 − 1]. Si n = 32 bits, entonces el intervalo de
enteros reprersentables es [−231 , 231 − 1] = [−2. 147. 483. 648, 2. 147. 483. 647]
Las constantes literales de tipo int se expresan en base 10 de forma similar a como estamos
acostumbrados en matemáticas. El signo positivo "+" es opcional y suele ser omitido.
Ejemplos: -2147483648, -21702400, 0, 14762, +14762, 9990219, 2147483647
El lenguaje facilita los siguientes operadores aritméticos para realizar cálculos con datos
enteros. Se presentan ordenados de mayor a menor prioridad.
64
Desbordamiento (overflow ) en un cálculo
El código anterior presenta por pantalla los siguiente resultados al ser ejecutado:
1! = 1
2! = 2
3! = 6
4! = 24
. . .
10! = 3628800
11! = 39916800
12! = 479001600
13! = 1932053504
14! = 1278945280
15! = 2004310016
16! = 2004189184
17! = −288522240
18! = −898433024
Los seis últimos resultados son erróneos. El origen del problema está en el cálculo de
13! = 6. 227. 020. 800. Este valor queda fuera del rango de los valores representables por el
tipo int, el intervalo [−2. 147. 483. 648, 2. 147. 483. 647]. El resultado presentado es erróneo ya
que se ha producido un error de desbordamiento u overflow .
65
6.2.1. Problemas que requieren el tratamiento cifra a cifra de números
enteros
Los humanos estamos acostumbrados a expresar las magnitudes enteras en base 10 ya que son
10 los dedos de nuestras dos manos. Ası́, por ejemplo, escribimos 365 para expresar los dı́as que
tiene un año o 1492 para referirmos al año en el que las carabelas de Cristóbal Colón llegaron a
América.
Las funciones que se presentan a continuación dan una respuesta algorı́tmica a los problemas
anteriores y constituyen buenos ejemplos que ilustran cómo trabajar con datos enteros para
resolver problemas que requieren analizar las cifras del número, cuando éste se expresa en base
10.
/∗
∗ Pre: −−−
∗ Post: Devuelve el número de cifras de <n> cuando este número se escribe en base 10
∗/
int numCifras (int n) {
int cuenta = 1; // lleva la cuenta de las cifras identificadas
n = n / 10; // elimina la cifra menos significativa de <n>
while (n != 0) {
cuenta = cuenta + 1; // contabliza la cifra menos significativa de <n>
n = n / 10; // y la elimina de <n>
}
return cuenta;
}
A continuación se presentan dos tablas que muestran la evolución de los valores de las variables
n y cuenta durante la ejecución de la función numCifras(n). La primera tabla corresponde a
una ejecución de la función para un valor inicial del parámetro n igual a 65030 y la segunda
corresponde a un valor inicial de n igual a -103044.
n cuenta
n cuenta
-103044
65030
-10304 1
6503 1
-1030 2
650 2
-103 3
65 3
-10 4
6 4
-1 5
0 5
0 6
66
Suma de las cifras de un entero expresado en base 10
/∗
∗ Pre: −−−
∗ Post: Devuelve la suma de las cifras de <n> cuando éste se escribe en base 10
∗/
int sumaCifras (int n) {
if (n < 0) {
n = −n; // como <n> es negativo, lo cambia de signo
}
int suma = 0; // suma de las cifras eliminadas de <n> (inicialmente 0)
while (n != 0) {
suma = suma + n % 10; // suma la cifra menos significativa de <n>
n = n / 10; // y la elimina de <n>
}
return suma;
}
Al igual que para la función anterior, se presentan dos tablas que muestran la evolución de
los valores de las variables n y suma durante la ejecución de sumaCifras(n). La primera tabla
corresponde la ejecución de sumaCifras(65030) y la segunda corresponde a la ejecución de
sumaCifras(-103044).
n suma
n suma -103044
65030 0 103044 0
6503 0 10304 4
650 3 1030 8
65 3 103 8
6 8 10 11
0 14 1 11
0 12
67
Cálculo de la i-ésima cifra menos significativa de un entero expresado en base 10
/∗
∗ Pre: i >= 1
∗ Post: Devuelve la i−esima cifra menos significatica de <n> cuando éste se escribe en
∗ base 10
∗/
int cifra (int n, int i ) {
if (n < 0) {
n = −n; // como <n> es negativo, lo cambia de signo
}
for (int exponente = 1; exponente < i; ++exponente) {
n = n / 10; // elimina la cifra menos significativa de <n>
}
return n % 10;
}
Al igual que en los dos casos anteriores, se presentan dos tablas que muestran la evolución de
los valores de las variables n, exponente e i durante la ejecución de la función cifra(n,i). La
primera tabla corresponde a una ejecución de la función para un valor inicial de los parámetros
n igual a 6470431 e i igual 3 y la segunda corresponde a un valor inicial de n igual a -6470431 y
a un valor de i igual a 5. El bucle concluye al igualarse los valores de las variables exponente e
i. La función devuelve el valor de la última cifra del entero almacenado en ese momento en la
variable n (se ha señalado con letra negrita en ambas tablas).
n exponente i
-6470431 5
n exponente i
6470431 1 5
6470431 1 3
647043 2 5
647043 2 3
64704 3 5
64704 3 3
6470 4 5
647 5 5
68
Cálculo de la imagen especular de un entero expresado en base 10
La función imagen(n) ilustra como construir, cifra a cifra, un número entero que sea la
imagen especular de otro cuando ambos son escritos en base 10.
/∗
∗ Pre: −−−
∗ Post: Devuelve el número que escrito en base 10 es la imagen especular de <n> cuando
∗ éste se escribe también en base 10.
∗/
int imagen (int n) {
// Si <n> es negativo lo cambia de signo
Al igual que en los problemas anteriores, se presentan dos tablas que muestran la evolución
de los valores de las variables n y suImagen durante la ejecución de la función imagen(n). La
primera tabla corresponde a una ejecución de la función para un valor inicial del parámetro n
igual a 6470431 y la segunda para un valor inicial igual a -60900.
69
n suImagen
n suImagen
6470431 0
-60900
647043 1
60900 0
64704 13
6090 0
6470 134
609 0
647 1340
60 9
64 13407
6 90
6 134074
0 906
0 1340746
70
6.2.2. Problemas sobre divisibilidad de números enteros
Un número entero es primo si es igual o mayor que 2 y no es divisible más que por la unidad
y por sı́ mismo. Los números primos son { 2, 3, 5, 7, 11, 13, 17, 19, 23, . . . }. A partir de estas
propiedades es sencillo diseñar la función esPrimo(n) con un único parámetro, n, que devuelve
un valor booleano true si y sólo si n satisface las condiciones anteriores.
En caso contrario analiza si n es menor que 2 o es divisible por 2. En ambos casos se concluye
que n no es primo.
/∗
∗ Pre: −−−
∗ Post: Devuelve [true] si y sólo si <n> es un número primo
∗/
bool esPrimo (int n) {
if (n == 2) {
return true; // <n> es igual a 2, luego es primo
}
else if (n < 2 || n % 2 ==0 ) {
return false; // <n> es menor que 2 o divisible por 2
}
else {
// Se buscan posibles divisores impares de <n> a partir de 3
int divisor = 3; // Primer divisor impar a probar
bool puedeSerlo = true;
while (puedeSerlo && divisor ∗ divisor <= n) {
puedeSerlo = n % divisor > 0;
divisor = divisor + 2;
}
return puedeSerlo;
}
}
71
Cálculo del máximo común divisor de un par de enteros
La programación de una función, mcd(a,b), que calcule el máximo común divisor aplicando
el algoritmo de Euclides es inmediata.
/∗
∗ Pre: a != 0 o b != 0
∗ Post: Devuelve el máximo común divisor de <a> y <b>
∗/
int mcd (int a, int b) {
if (a < 0) {
a = −a;
}
if (b < 0) {
b = −b;
}
// Aplica el algoritmo de Euclides para el calculo del m.c.d. de <a> y <b>
while (b != 0) {
int resto = a % b;
a = b;
b = resto;
}
return a;
}
72
Capı́tulo 7
El objetivo principal de este capı́tulo es enseñar los fundamentos del diseño de programas
modulares desarrollados aplicando una metodologı́a de diseño descendente. Ilustra también
cómo diseñar, aplicando las ideas anteriores, un programa dirigido por menú.
Al ser ejecutado, el programa planteará reiteradamente al operador un menú con seis opciones.
La primera de ellas, la opción 0, fuerza la finalización de la ejecución del programa. Las cinco
opciones restantes permiten seleccionar diferentes cálculos con enteros.
Un ejemplo ilustrativo del diálogo que debe mantener el programa con el operador se presenta
a continuación. Las respuestas del operador se han destacado con sus caracteres subrayados.
MENU DE OPERACIONES
===================
0 - Finalizar
1 - Calcular el número de cifras de un entero
2 - Sumar las cifras de un entero
3 - Extraer una cifra de un entero
4 - Calcular la imagen especular de un entero
5 - Comprobar si un entero es primo
73
MENU DE OPERACIONES
===================
0 - Finalizar
1 - Calcular el número de cifras de un entero
2 - Sumar las cifras de un entero
3 - Extraer una cifra de un entero
4 - Calcular la imagen especular de un entero
5 - Comprobar si un entero es primo
MENU DE OPERACIONES
===================
0 - Finalizar
1 - Calcular el número de cifras de un entero
2 - Sumar las cifras de un entero
3 - Extraer una cifra de un entero
4 - Calcular la imagen especular de un entero
5 - Comprobar si un entero es primo
MENU DE OPERACIONES
===================
0 - Finalizar
1 - Calcular el número de cifras de un entero
2 - Sumar las cifras de un entero
3 - Extraer una cifra de un entero
4 - Calcular la imagen especular de un entero
5 - Comprobar si un entero es primo
MENU DE OPERACIONES
===================
0 - Finalizar
1 - Calcular el número de cifras de un entero
2 - Sumar las cifras de un entero
3 - Extraer una cifra de un entero
4 - Calcular la imagen especular de un entero
5 - Comprobar si un entero es primo
74
7.2. Estructura modular y diseño descendente del programa
Hasta este capı́tulo, un único fichero almacenaba el código de cada uno de los programas
desarrollados. Este fichero es denominado módulo de programa ya que contiene el código de
la función main(), función principal del programa. Pero un programa puede contar con módulos
adicionales, denominados módulos de biblioteca.
Un módulo principal en el que se define, como mı́nimo, su función main() y que puede
utilizar recursos de otros módulos de biblioteca.
Uno o más módulos de biblioteca en los que se definen diferentes recursos que se ponen a
disposición de otros módulos: datos constantes, datos variables, tipos de datos y funciones.
La definición de un módulo de biblioteca consta de dos partes, cada una de las cuales se
almacena en un fichero independiente:
• Interfaz del módulo. En ella se declaran los recursos públicos de este módulo que
pueden ser utilizados en otros módulos.
La interfaz de un módulo se define en el fichero de cabecera del módulo que ha de
tener como sufijo ".h".
La declaración de una función se limita a tres elementos, el tipo de dato devuelto,
su nombre y su lista de parámetros, y concluye con el sı́mbolo finalizador ";".
Conviene que la declaración de toda función vaya precedida por un comentario
con la especificación de su comportamiento. La declaración de una función en
C++ se denomina prototipo. Ejemplo de especificación o prototipo de la función
numCifras(n):
/∗
∗ Pre: −−−
∗ Post: Devuelve el número de cifras de <n> cuando este número se escribe en base 10
∗/
int numCifras (int n);
Las ideas anteriores se van a precisar e ilustrar al aplicarlas en los siguientes apartados al
diseño del programa propuesto en el apartado anterior. El programa a diseñar constará de dos
módulos:
75
programa, junto con otras funciones auxiliares resultantes del diseño descendente del
programa que se explica más adelante. Este módulo principal hará uso de algunas de
las funciones de cálculo con enteros definidas en el módulo de biblioteca que se presenta a
continuación.
/∗
∗ Fichero calculos .cc de implementación del módulo calculos
∗/
/∗
∗ Pre: −−−
∗ Post: Devuelve el número de cifras de <n> cuando este número se escribe en base 10
∗/
int numCifras (int n) {
int cuenta = 1; // lleva la cuenta de las cifras identificadas
n = n / 10; // elimina la cifra menos significativa de <n>
while (n != 0) {
// El valor de <cuenta> es igual al de cifras identificadas en <n>
cuenta = cuenta + 1; // contabiliza la cifra menos significativa de <n>
n = n / 10; // y la elimina de <n>
}
return cuenta;
}
/∗
∗ Pre: −−−
∗ Post: Devuelve la suma de las cifras de <n> cuando éste se escribe en base 10
∗/
int sumaCifras (int n) {
if (n < 0) {
n = −n; // si <n> es negativo, le cambia el signo
}
int suma = 0; // suma de las cifras eliminadas de <n> (inicialmente 0)
while (n != 0) {
suma = suma + n % 10; // suma la cifra menos significativa de <n>
n = n / 10; // y la elimina de <n>
}
return suma;
}
...
76
...
/∗
∗ Pre: i >= 1
∗ Post: Devuelve la i−esima cifra menos significatica de <n> cuando éste se escribe
∗ en base 10
∗/
int cifra (int n, int i ) {
if (n < 0) {
n = −n; // si <n> es negativo, le cambia el signo
}
for (int exp = 1; exp < i; ++exp) {
n = n / 10; // elimina la cifra menos significativa de <n>
}
return n % 10;
}
/∗
∗ Pre: −−−
∗ Post: Devuelve el número que escrito en base 10 es la imagen especular de <n>
∗ cuando éste se escribe también en base 10.
∗/
int imagen (int n) {
// <negativo> memoriza si <n> es positivo o negativo
bool negativo = n < 0;
if (n < 0) {
n = −n; // si <n> es negativo, le cambia el signo
}
// Calcula el dı́gito más significativa de la imagen de <n>
int suImagen = n % 10;
n = n / 10;
while (n != 0) {
// Incorpora el dı́gito menos significativo de <n> a <suImagen>
suImagen = 10 ∗ suImagen + n % 10;
// Y lo elimina de <n>
n = n / 10;
}
// Si <n> era inicialmente negativo devuelve el valor de <suImagen>
// cambiado de signo y si era inicialmente positivo devuelve el valor
// de <suImagen>
if (negativo) {
return −suImagen;
}
else {
return suImagen;
}
}
...
77
...
/∗
∗ Pre: n >= 0
∗ Post: Devuelve el factorial de <n>
∗/
int factorial (int n) {
int r = 1; // r = 0!
for (int i = 2; i <= n; ++i) {
// r = (i−1)!
r = i ∗ r;
// r = i!
}
// r = n!
return r;
}
/∗
∗ Pre: −−−
∗ Post: Devuelve <true> si y sólo si <n> es un número primo
∗/
bool esPrimo (int n) {
if (n == 2) {
return true; // <n> es igual a 2, luego es primo
}
else if (n < 2 || n % 2 == 0) {
return false; // <n> es menor que 2 o divisible por 2
}
else {
// Se buscan posibles divisores impares de <n> a partir del 3
int divisor = 3; // Primer divisor impar a probar
bool loParece = true;
while (loParece && divisor ∗ divisor <= n) {
loParece = n % divisor > 0;
divisor = divisor + 2;
}
return loParece;
}
}
/∗
∗ Pre: a != 0 ó b != 0
∗ Post: Devuelve el máximo común divisor de <a> y <b>
∗/
int mcd (int a, int b) {
if (a < 0) {
a = −a;
}
if (b < 0) {
b = −b;
}
while (b != 0) {
int resto = a % b;
a = b;
b = resto;
}
return a;
78
}
/∗
∗ Fichero calculos .h de intefaz del módulo calculos
∗/
/∗
∗ Pre: −−−
∗ Post: Devuelve el número de cifras de <n> cuando este número se escribe en base 10
∗/
int numCifras (int n);
/∗
∗ Pre: −−−
∗ Post: Devuelve la suma de las cifras de <n> cuando éste se escribe en base 10
∗/
int sumaCifras (int n);
/∗
∗ Pre: i >= 1
∗ Post: Devuelve la i−esima cifra menos significatica de <n> cuando éste se escribe
∗ en base 10
∗/
int cifra (int n, int i );
/∗
∗ Pre: −−−
∗ Post: Devuelve el número que escrito en base 10 es la imagen especular de <n>
∗ cuando éste se escribe también en base 10.
∗/
int imagen (int n);
/∗
∗ Pre: n >= 0
∗ Post: Devuelve el factorial de <n>
∗/
int factorial (int n);
/∗
∗ Pre: −−−
∗ Post: Devuelve <true> si y sólo si <n> es un número primo
∗/
bool esPrimo (int n);
/∗
∗ Pre: a != 0 ó b != 0
∗ Post: Devuelve el máximo común divisor de <a> y <b>
∗/
int mcd (int a, int b);
79
7.2.2. Diseño descendente del módulo principal
Un resumen de las ideas anteriores sobre el diseño descendente del programa se presenta a
continuación.
Un listado del código del módulo principal del programa se presenta a continuación.
#include <iostream>
#include ”calculos.h”
using namespace std;
/∗
∗ Pre: −−−
∗ Post: Presenta el menu de opciones disponibles
∗/
void presentarMenu () {
cout << endl << ”MENU DE OPERACIONES” << endl;
cout << ”===================” << endl;
cout << ”0 − Finalizar” << endl;
cout << ”1 − Calcular el numero de cifras de un entero” << endl;
cout << ”2 − Sumar las cifras de un entero” << endl;
cout << ”3 − Extraer una cifra de un entero” << endl;
cout << ”4 − Calcular la imagen especular de un entero” << endl;
cout << ”5 − Comprobar si un entero es primo” << endl << endl;
}
...
80
...
/∗
∗ Pre: −−−
∗ Post: Ejecuta las acciones asociadas a la orden cuyo código es <operacion>
∗/
void ejecutarOrden (int operacion) {
if (operacion >= 1 && operacion <= 5) {
// Se va a ejcutar una operación válida. En primer lugar se pide al operador
// que defina un número entero
int numero;
cout << ”Escriba un numero entero : ” << flush;
cin >> numero;
if (operacion == 1) {
// Informa del número de cifras de <numero>
cout << ”El numero ” << numero << ” tiene ” << numCifras(numero)
<< ” cifras” << endl;
}
else if (operacion == 2) {
// Informa de la suma de las cifras de <numero>
cout << ”Las cifras de ” << numero << ” suman ” << sumaCifras(numero)
<< endl;
}
else if (operacion == 3) {
// El operador debe definir la posición de una cifra de <numero>
int posicion ;
cout << ”Seleccione la posicion de una cifra: ” << flush;
cin >> posicion;
// Informa del valor de la cifra ubicada en <posicion> de <numero>
cout << ”La cifra situada en la posicion ” << posicion << ” del numero ”
<< numero << ” es ” << cifra(numero, posicion) << endl;
}
else if (operacion == 4) {
// Informa del valor de la imagen especular de <numero>
cout << ”El numero imagen especular del ” << numero << ” es el ”
<< imagen(numero) << endl;
}
else if (operacion == 5) {
// Informa si <numero> es un número primo o no lo es
cout << ”El numero ” << numero;
if (! esPrimo(numero)) {
cout << ” no”;
}
cout << ” es primo” << endl;
}
}
else {
// El código de operación no es válido
cout << ”Opción desconocida” << endl;
}
}
...
81
...
/∗
∗ Plantea al operador de forma reiterada un menú con varias opciones. En cada
∗ iteración lee la respuesta del operador y presenta los resultados de ejecutar
∗ la opción elegida . Concluye cuando el operador selecciona la opción [0]
∗/
int main () {
// Presenta por primera vez el menú de opciones
presentarMenu();
// Pide al operador que escriba el código de la primera operación
cout << ”Seleccione una operacion [0−5]: ” << flush;
// Asigna a <operacion> la respuesta del operador
int operacion;
cin >> operacion;
// Itera hasta que el valor de <operacion> sea igual a 0
while (operacion != 0) {
// Ejecuta la última operación seleccionada
ejecutarOrden(operacion);
// Presenta de nuevo el menú de opciones
presentarMenu();
// Pide al operador que escriba el código de una nueva operación
cout << endl << ”Seleccione una operacion [0−5]: ” << flush;
// Asigna a <operacion> la nueva respuesta del operador
cin >> operacion;
}
// El programa concluye normalmente
return 0;
}
82
Capı́tulo 8
Son frecuentes los problemas de tratamiento de información en los que se requiere hacer
cálculos con números reales. En este capı́tulo se presentan las herramientas que el lenguaje C++
pone a disposición del programador para trabajar con datos reales, se explican las limitaciones
que entraña operar con reales y se aborda la resolución de algunos problemas ilustrativos de
cálculo con reales.
El lenguaje C++ ofrece tres tipos de datos diferentes para representar números reales: float,
double y long double. Nos limitaremos en este curso a hacer uso del tipo double cuyas
caracterı́sticas son las siguientes:
Dedica sizeof(double) bytes para representar internamente un dato real. Aunque este
número depende de cada implementación, suele ser usual que sizeof(double) = 8 bytes,
que corresponde a 8 bits/byte × sizeof(double) bytes = 8 bits/byte × 8 bits = 64 bits.
Utiliza un código binario en coma flotante de doble precisión según norma IEEE 754. Una
implementación del tipo double con 8 bytes permite representar números reales dentro
del intervalo [−1. 7 × 10308 , 1. 7 × 10308 ]
El menor real representable mayor que 0.0 es el 2. 23 × 10−308
Sus constantes literales se expresan como los números reales en matemáticas, pudiendo
utilizarse la notación cientı́fica. Ejemplos: 12.1542, -12.1542, 0.0, 6.023e23, 0.57e-7,
-1.92826e7
Gestiona una precisión de 15 dı́gitos decimales significativos.
El lenguaje pone a disposición del programador las siguientes operaciones para operar con
reales de tipo double.
• Operaciones de relación para comparar pares de datos, e1 y e2, del mismo tipo:
e1 == e2, e1 != e2, e1 <e2, e1 <= e2, e1 >e2 y e1 >= e2
• Operaciones de signo que se aplican a un único dato, e: +e y -e
• Operaciones aritméticas que se aplican a pares de datos, e1 y e2, del mismo tipo:
e1 + e2, e1 - e2, e1 * e2 y e1 / e2
83
Desbordamiento (overflow ) y precisión en un cálculo
Al evaluar el resultado de una operación aritmética entre datos reales pueden plantearse dos
tipos de problemas que conviene conocer y saber valorar sus consecuencias:
Los dos problemas anteriores quedan ilustrados al ejecutar el código que se presenta a
continuación.
923.47ˆ1 = 923.47
923.47ˆ2 = 852797
923.47ˆ3 = 7.87532e+008
923.47ˆ4 = 7.27262e+011
...
923.47ˆ100 = 3.48561e+296
923.47ˆ101 = 3.21886e+299
923.47ˆ102 = 2.97253e+302
923.47ˆ103 = 2.74503e+305
923.47ˆ104 = inf
923.47ˆ105 = inf
923.47ˆ106 = inf
... ...
84
Todos los resultados se presentan con un máximo de siete cifras significativas. Se podrı́an
presentar con más cifras significativas, pero siempre teniendo en cuenta que, tras cada cálculo,
no se puede lograr una precisón mayor que hasta 16 cifras significativas. Este hecho tiene dos
consecuencias que conviene comprender y, en su caso, tener en cuenta:
Sólo "unos pocos" valores reales son representables exactamente como datos de tipo
double. Al intentar representar cualquier resultado como dato de tipo double hay que
asumir una posible falta de precisión.
Funciones trigonométricas:
• double floor (double x) - Devuelve el mayor real sin decimales que sea menor
o igual que x
• double ceil (double x) - Devuelve el menor real sin decimales que sea mayor
o igual que x
• double round (double x) - Devuelve el real sin decimales más próximo a x
85
• double trunc (double x) - Devuelve el real resultante de eliminar los decimales
de x
/∗
∗ Pre: −−−
∗ Post: Presenta por pantalla una lı́nea con los resultados de aplicar las siguientes
∗ funciones al parámetro x: floor (x), ceil (x), round(x) y trunc(x)
∗/
void mostrarResultadosFunciones(double x) {
cout << right << setw(10) << x << setw(10) << floor(x) << setw(10) << ceil(x)
<< setw(10) << round(x) << setw(10) << trunc(x) << endl;
}
86
Y este serı́a el listado con los resultados escritos por pantalla al ejecutar el código anterior.
En este apartado se presenta una colección de problemas de cálculo con reales resueltos cada
uno de ellos mediante el diseño de una función C++.
/∗
∗ Pre: nLados >= 3 y lado > 0.0
∗ Post: Dado un polı́gono regular con <nLados> lados de longitud <lado>:
∗ − <perimetro> tiene asignado el valor del perı́metro del polı́gono
∗ − <area> tiene asignado el valor del área del polı́gono
∗ − <angulo> tiene asignado el valor, en grados sexagesimales, del ángulo
∗ determinado por dos lados adyacentes del polı́gono
∗/
void poligono (int nLados, double lado,
double &perimetro, double &area, double &angulo) {
const double PI = 3.141593; // definición de la constante PI
double alfa = PI / nLados; // calcula el semiángulo central
double apotema = lado / (2.0 ∗ tan(alfa)); // calcula la apotema del polı́gono
perimetro = nLados ∗ lado; // calcula el perı́metro del polı́gono
area = perimetro ∗ apotema / 2.0; // calcula el área del polı́gono
angulo = (nLados − 2) ∗ 180.0 / nLados; // calcula el ángulo interior del polı́gono
}
87
8.2.2. Resolución de una ecuación de segundo grado
Las raı́ces de la ecuación pueden ser bien dos números reales o bien un par de números
complejos conjugados.
Primera raı́z: 2
Segunda raı́z: 1
Y cuando las dos raı́ces de la ecuación sean dos números complejos conjugados, como es en
el caso de la ecuación x2 − 2x + 5 = 0, la función las escribirá por pantalla del siguiente modo:
/∗
∗ Pre: −−−
∗ Post: Presenta por pantalla las dos raices de la ecuación de segundo grado:
∗ a.xˆ2 + b.x + c = 0
∗ Si las dos raices son reales , r1 y r2, las presenta ası́ :
∗ Primera raiz: r1
∗ Segunda raiz: r2
∗ Si las dos raices son complejas, a+b.i y a−b.i, las presenta ası́ :
∗ Primera raiz. Parte real : a Parte imaginaria: b
∗ Segunda raiz: Parte real : a Parte imaginaria: −b
∗/
void ecuacionSegundoGrado (double a, double b, double c) {
double discriminante = b ∗ b − 4.0 ∗ a ∗ c;
double termino1 = −b / (2.0 ∗ a);
double termino2 = sqrt(abs(discriminante)) / (2.0 ∗ a);
cout << endl;
if (discriminante >= 0.0) {
// Presenta las dos raices reales de la ecuación
cout << ”Primera raiz: ” << termino1 + termino2 << endl
<< ”Segunda raiz: ” << termino1 − termino2 << endl;
}
else{
// Presenta las dos raices complejas conjugadas de la ecuación
cout << ”Primera raiz. Parte real: ” << termino1
<< ” Parte imaginaria: ” << termino2 << ”i” << endl
<< ”Segunda raiz. Parte real: ” << termino1
<< ” Parte imaginaria: ” << −termino2 << ”i” << endl;
}
}
88
8.2.3. Sumas de series
La suma de series numéricas es un problema tı́pico de cálculo iterativo con números reales.
En este apartado se presentan varios ejemplos de funciones que suman series numéricas.
/∗
∗ Pre: −−−
∗ Post: Devuelve eˆx
∗/
double exponencial (double x) {
// Se tiene en cuenta que el desarrollo en serie de la función exponencial es:
// eˆx = 1 + x/1! + xˆ2/2! + xˆ3/3! + ... + xˆn/n! + ...
const double COTA = 1.0e−8;
// Se asigna a <valor> el primer término de la serie, es decir , 1.0
double termino = 1.0,
indice = 0.0,
valor = termino;
while (abs(termino) > COTA) {
// Se incrementa <valor> con el siguiente término de la serie
indice = indice + 1.0;
termino = x ∗ termino / indice; // termino = xˆindice/indice!
valor = valor + termino; // valor = 1 + ... + xˆindice/indice!
}
return valor;
}
89
Cálculo aproximado del valor de la función seno
El valor de la función seno, sen x, puede calcularse de forma aproximada sumando un número
suficientemente grande de pares de términos de la serie alternada:
x3 x5 x7 x9 x11 2n−1
x
sen x = x − 3! + 5! − 7! + 9! − 11! + · · · + (−1)n−1 (2n−1)! + ...
La función sen(x) devuelve un valor aproximado del sen x. En ella se van sumando términos
de la serie. Concluye el algoritmo cuando el valor absoluto del incremento del valor de la serie
aportado en la última iteración es inferior al de la constante COTA.
/∗
∗ Pre: El valor de <x> viene expresado en radianes
∗ Post: Devuelve sen x
∗/
double sen (double x) {
// Se tiene en cuenta que el desarrollo en serie de la función seno es:
// sen x = x − xˆ3/3! + xˆ5/5! − xˆ7/7! + xˆ9/9! − xˆ11/11! + ...
const double COTA = 1.0e−10;
// Se asigna a <valor> el primer término de la serie
double indice = 1.0, // ı́ndice del primer término
potencia = x, // potencia = xˆindice
denominador = 1.0, // denominador = indice!
termino = x, // termino = (−1)ˆ(indice/2).xˆindice/indice!
valor = x; // valor = suma de los terminos calculados
while (abs(termino) > COTA) {
// Se incrementa <valor> con el siguiente término de la serie
indice = indice + 2.0; // ı́ndice del siguiente término
potencia = −x ∗ x ∗ potencia; // potencia = xˆindice
denominador = denominador ∗ (indice − 1.0) ∗ indice; // denominador = indice!
termino = potencia / denominador; // termino = (−1)ˆ(indice/2).xˆindice/indice!
valor = valor + termino; // valor = suma de los terminos calculados
}
// Devuelve la suma de los términos de la serie acumulados en <valor>
return valor;
}
90
Cálculo aproximado del valor de la constante π
Existen varios métodos para calcular el valor de la constante trigonométrica π. Uno de ellos,
bastante sencillo aunque no el mejor, consiste en sumar la serie de Leibnitz, que es la siguiente
serie alternada:
1 1 1 1 1 1
π = 4(1 − 3 + 5 − 7 + 9 − 11 + · · · + (−1)n−1 2n−1 + ...)
La función pi() devuelve un valor aproximado del número π. De forma similar a la función
anterior, en ésta se van sumando pares de términos consecutivos de la serie. Concluye el algoritmo
cuando el valor absoluto del incremento del valor de la serie aportado en la última iteración es
inferior al de la constante COTA.
/∗
∗ Pre: −−−
∗ Post: Devuelve un valor aproximado del número ”PI”
∗/
double pi() {
// Se tiene en cuenta la aproximación del número ”PI” como suma de la serie
// alternada de Leibnitz :
// PI = 4(1 − 1/3 + 1/5 − 1/7 + 1/9 − 1/11 + ... )
const double COTA = 1.0e−12;
// Se asigna a [suma] la suma de los dos primeros términos de la serie :
// 1 − 1/3 + 1/5 − 1/7 + 1/9 − 1/11 + ...
int indice = 3;
double incremento = 2.0/3.0;
double suma = incremento;
while (abs(incremento) > COTA) {
// Se incrementa <suma> con la suma de los dos siguientes términos de la serie
indice = indice + 4;
incremento = 1.0 / (indice−2) − 1.0 / indice;
suma = suma + incremento;
}
return 4.0 ∗ suma;
}
91
Capı́tulo 9
En este capı́tulo se trabaja con colecciones de datos del mismo tipo, gestionadas como
estructuras de datos indexadas que denominaremos genéricamente tablas, vectores o arrays.
Definiremos y crearemos tablas de datos y diseñaremos funciones que resuelvan algunos de los
problemas que se presentan con más frecuencia cuando trabajamos con tablas.
Las tablas son estructuras de datos cuyo fin es almacenar en memoria una colección de datos
de un mismo tipo, denominados elementos de la tabla. Las tablas son estructuras indexadas
mediante uno o más ı́ndices. Los ı́ndices de una tabla en C++ toman valores enteros consecutivos
a partir del 0, es decir, 0, 1, 2, 3, etc.. Una tabla puede almacenar datos, todos ellos del mismo
tipo, o punteros a datos del mismo tipo (los punteros se estudiarán en asignaturas posteriores).
La definición de una tabla comprende la declaración del tipo de sus elementos, la declación
del nombre de la tabla y, opcionalmente, la declaración del tamaño de la tabla, entre corchetes.
He aquı́ un ejemplo en el que se declaran dos tablas, la tabla unidimensional T que almacena N
datos de tipo int y la tabla bidimensional M que almacena una matriz de NF×NC datos de tipo
double.
La creación de una tabla comporta la reserva de memoria para almacenar los valores de sus
elementos. Si no se especifican dichos valores, estarán inicialmente indefinidos.
También es posible definir una tabla especificando el valor inicial de cada uno de sus elementos.
En tales casos la primera dimensión de la tabla puede omitirse; sólo es necesario especificar, en
su caso, las restantes dimensiones.
93
{ 0, 1, 0 },
{ 0, 0, 1 }
};
M
v
0 1 ... ... NC-2 NC-1
0 d1
0 d1,1 d1,2 ... ... d1,N C−1 d1,N C
1 d2
1 d2,1 d2,2 ... ... d2,N C−1 d2,N C
... ...
... ... ... ... ... ... ...
... ...
... ... ... ... ... ... ...
N-2 dN −1
NF-2 dN F −1,1 dN F −1,2 ... ... dN F −1,N C−1 dN F −1,N C
N-1 dN
NF-1 dN F,1 dN F,2 ... ... dN F,N C−1 dN F,N C
9.2.1. Selección de sus elementos y las tablas como parámetros de una función
94
parámetro cuyo valor representa el número de elementos de la tabla con los que se va a
trabajar, los elementos cuyos ı́ndices pertenecen al intervalo [0, n − 1].
#include <iostream>
#include <iomanip> // función setw
#include <time.h> // función time
#include <stdlib.h> // funciones srand y rand
/∗
∗ Pre: El número de elementos del vector <v> es igual o mayor que <n>
∗ Post: Asigna a los elementos de v [0.. n−1] valores pseudoaleatorios comprendidos
∗ entre las constantes MIN y MAX
∗/
void definirValores (int v [], int n) {
// Lı́mites del intervalo de números pseudoaleatorios a generar
const int MIN = −100,
MAX = 100;
// Inicializa la serie de números pseudoaleatorios en función del tiempo
srand (time(NULL));
// Asigna a los elementos de v [0.. n−1] valores pseudoaleatorios comprendidos entre
// MIN y MAX
for (int i = 0; i < n; ++i) {
v[ i ] = MIN + rand() % (MAX − MIN + 1);
}
}
/∗
∗ Pre: El número de elementos del vector <v> es igual o mayor que <n>
∗ Post: Presenta por pantalla los valores de los elementos de v [0.. n−1]
∗ en NCOL columnas de un número de caracteres igual a ANCHO
∗/
void mostrarValores (int v [], int n) {
// Número de columnas y anchura de cada columna
const int NCOL = 10,
ANCHO = 6;
// Presentación por pantala de los elementos de v [0.. n−1]
for (int i = 0; i < n; ++i) {
cout << setw(ANCHO) << v[i];
if ( i % NCOL == NCOL − 1) {
cout << endl;
}
}
// Acaba, si es necesario, la última lı́nea y añade una lı́nea en blanco adicional
if (n % NCOL != 0) {
cout << endl ;
}
cout << endl;
}
...
95
...
/∗
∗ Pre: El número de elementos del vector <v> es igual o mayor que <n>
∗ Post: Sustituye por 0 los elementos de v [0.. n−1] cuyo valor previo sea negativo y deja
∗ inalterados los restantes elementos de v [0.. n−1]
∗/
void filtrar (int v [], int n) {
// Anula los elementos de v [0.. n−1] cuyo valor sea negativo
for (int i = 0; i < n; ++i) {
if (v[ i ] < 0) {
v[ i ] = 0;
}
}
}
/∗
∗ Programa que ilustra el trabajo con tablas unidimensionales (vectores) de datos enteros:
∗ creación de una tabla, recorrido de una tabla para mostrar sus elementos y modificación
∗ del valor de algunos elementos de la tabla
∗/
int main () {
const int DIM = 100; // Dimensión de la tabla a crear
int v [DIM]; // Crea la tabla <v> de datos enteros
definirValores (v,DIM); // Asigna valor a los elementos de la tabla <v>
mostrarValores(v,DIM); // Muestra por pantalla los elementos de la tabla <v>
filtrar (v,DIM); // Modifica alguno de los elementos de la tabla <v>
mostrarValores(v,DIM); // Muestra por pantalla los elementos de la tabla <v>
return 0;
}
En este apartado se van a presentar algunas funciones que resuelven problemas haciendo uso
de la información almacenada previamente en una tabla de datos.
Obsérvese cómo se puede asignar valores iniciales a los elementos de la tabla TABLA PRIMOS
al ser creada y que, en tal caso, no se debe especificar el tamaño de la tabla, ya que viene
definido por el número de valores almacenados en ella. Obsérvese también que TABLA PRIMOS
se ha definido como una constante, es decir, como una tabla que no admitirı́a ser modificada
durante la ejecución del código de la función.
96
/∗
∗ Pre: n < 100
∗ Post: Devuelve <true> si y sólo si <n> es un número primo
∗/
bool esPrimo (int n) {
const int NUM PRIMOS = 25;
const int TABLA PRIMOS[] = { 2, 3, 5, 7, 11, 13, 17, 19, 23, 29,
31, 37, 41, 43, 47, 53, 59, 61, 67, 71,
73, 79, 83, 89, 97 };
if (n < 2) {
return false;
}
else {
bool confirmadoQueEsPrimo = false;
int i = 0;
while (!confirmadoQueEsPrimo && i < NUM PRIMOS) {
if (TABLA PRIMOS[i] == n) {
confirmadoQueEsPrimo = true;
}
else {
i = i + 1;
}
}
return confirmadoQueEsPrimo;
}
}
Los números de identificación fiscal (NIF) de los ciudadanos españoles constan de dos datos,
el número de su documento nacional de identidad (DNI) y una letra mayúscula. La letra se
puede calcular a partir del valor del DNI. Basta calcular el resto de la división entre el número
de DNI y 23 y consultar en la tabla mostrada a continuación.
97
/∗
∗ Pre: −−−
∗ Post: Devuelve <true> si y sólo si el par (<dni>,<letra>) definen un número de
∗ identificación fiscal (NIF) válido
∗/
bool validar (int dni, char letra) {
const int NUM LETRAS = 23;
const char TABLA NIF[] = { ’T’, ’R’, ’W’, ’A’, ’G’, ’M’, ’Y’, ’F’, ’P’, ’D’,
’X’, ’B’, ’N’, ’J’ , ’Z’, ’S’ , ’Q’, ’V’, ’H’, ’L’,
’C’, ’K’, ’E’ };
return TABLA NIF[dni % NUM LETRAS] == letra;
}
La función letra(dni) devuelve la letra mayúscula que corresponde como letra del NIF
(número de identificación fiscal) a un número de documento nacional de identidad igual a dni.
Su diseño algorı́tmico se centra en la utilización de una tabla constante, TABLA NIF, que almacena
las 23 letras válidas de los NIFs, de forma análoga al caso de la función validar(dni,letra).
/∗
∗ Pre: −−−
∗ Post: Devuelve la letra del número de identificación fiscal que corresponde a un número
∗ de documento nacional de identidad igual a <dni>
∗/
char letra (int dni) {
const int NUM LETRAS = 23;
const char TABLA NIF[] = { ’T’, ’R’, ’W’, ’A’, ’G’, ’M’, ’Y’, ’F’, ’P’, ’D’,
’X’, ’B’, ’N’, ’J’ , ’Z’, ’S’ , ’Q’, ’V’, ’H’, ’L’,
’C’, ’K’, ’E’ };
return TABLA NIF[dni % NUM LETRAS];
}
Cuando se trabaja con vectores que almacenan datos numéricos puede ser necesario programar
algunos cálculos estadı́sticos. En este apartado se van a presentar tres funciones que permiten
calcular la media aritmética, la desviación tı́pica y la moda de una colección de datos numéricos
de tipo double almacenados en un vector.
98
La función media(v,n) devuelve la media aritmética de los primeros n elementos del vector
v, es decir, de los elementos de v[0..n-1].
/∗
∗ Pre: n > 0
∗ Post: Devuelve la media aritmética de los elementos de v [0.. n−1]
∗/
double media (double v[], int n) {
// En <suma> no se ha acumulado aún ninguun elemento del vector <v>
double suma = 0.0;
for (int i = 0; i < n; ++i) {
// Acumula en <suma> el valor del elemento i−ésimo del vector <v>
suma = suma + v[i];
}
// Calcula el valor promedio de los elementos sumados
return suma / n;
}
/∗
∗ Pre: n > 0
∗ Post: Devuelve la desviacion tipica de los elementos de v [0.. n−1]
∗/
double desviacion (double v[], int n) {
double mediaAritmetica = media(v,n),
suma = 0.0;
for (int i = 0; i < n; ++i) {
suma = suma + pow(v[i] − mediaAritmetica, 2);
}
return sqrt(suma / (n − 1));
}
99
9.3.3. Cálculo de la moda
En términos estadı́sticos, la moda de una distribución de datos es el valor con una mayor
frecuencia. Ası́, en la colección de datos { H, J, K, A, B, B, A, B, B, D, E, H, E} la moda es el
dato B, ya que su frecuencia, cuatro apariciones, es mayor que la de los restantes datos.
Se dice que una distribución es bimodal cuando encontramos dos modas, es decir, dos datos
que tienen la misma frecuencia absoluta máxima. La colección { B, D, E, E, A, J, A, E, H, B,
B, H, K } es bimodal ya que tanto B como E son moda de la colección. Una distribución es
trimodal si encontramos tres modas y ası́ sucesivamente.
/∗
∗ Pre: n > 0
∗ Post: Devuelve la moda de los elementos de v[0..n−1]
∗/
int moda (int v[], int n) {
// Toma como primer candidato a moda. el elemento v[0] y
// calcula el número de veces que está repetido en v[0,n−1]
int iModa = 0;
int repeticionesModa = 1;
for (int j = iModa + 1; j < n; ++j) {
if (v[iModa] == v[j]) {
repeticionesModa = repeticionesModa + 1;
}
}
// Analiza el número de repeticiones de los datos de v [1, n−1]
for (int i = 1; i < n; ++i) {
// Cuenta el número de veces que está repetido v[ i ] en v[ i ,n−1]
int repeticionesNuevo = 1;
for (int j = i + 1; j < n; ++j) {
if (v[ i ] == v[j]) {
repeticionesNuevo = repeticionesNuevo + 1;
}
}
// Si el número de repeticiones de v[ i ] supera el valor de <repeticionesModa>,
// entonces retiene v[ i ] como nuevo candidato a ser la moda
if (repeticionesModa < repeticionesNuevo) {
iModa = i;
}
}
// Devuelve el valor de la moda, v[iModa]
return v[iModa];
}
100
9.4. Trabajo con tablas multidimensionales
En un programa C++ se pueden crear tablas con más de un ı́ndice. El programa presentado a
continuación ilustra el trabajo con matrices cudradas cuyos elementos son datos enteros de tipo
int.
1. Crea una matriz cuadrada de elementos de tipo int de dimensión DIM×DIM, donde DIM es
una constante global del programa.
3. Presenta por pantalla el contenido de la matriz, a razón de una fila por lı́nea de pantalla.
5. Vuelve a presentar por pantalla el contenido de la matriz, a razón de una fila por lı́nea
de pantalla.
Cada una de las operaciones anteriores la realiza una función. La referencia a la matriz es
uno de los parámetros de cada una de estas funciones, como se puede comprobar en el código
del programa.
#include <iostream>
#include <iomanip>
#include <ctime> // función time
#include <cstdlib> // función rand
/∗
∗ Pre: DIM > 0 y <m> es una matriz cuadrada de dimension DIMxDIM
∗ Post: Asigna valores pseudoaleatorios comprendidos entre las constantes MIN y MAX
∗ a los elementos de la matriz <m>
∗/
void definirValores (int m[DIM][DIM]) {
// Lı́mites del intervalo de números pseudoaleatorios a generar
const int MIN = −10, MAX = 10;
// Inicializa la serie de números pseudoaleatorios en función del tiempo
srand (time(NULL));
// Asigna a los elementos de v [0.. n−1] valores pseudoaleatorios comprndidos entre
// MIN y MAX
for (int i = 0; i < DIM; ++i) {
for (int j = 0; j < DIM; ++j)
m[i ][ j ] = MIN + rand() % (MAX − MIN + 1);
}
101
}
...
102
...
/∗
∗ Pre: DIM > 0 y <m> es una matriz cuadrada de dimension DIMxDIM
∗ Post: Presenta por pantalla los elementos de la matriz <m> a razón de una fila por lı́nea,
∗ utilizando un número de caracteres igual a ANCHO para presentar cada elemento
∗/
void mostrarValores (int m[DIM][DIM]) {
// Anchura de cada columna
const int ANCHO = 6;
// Presentación por pantala de los elementos de la matriz <m>
// Presenta en una lı́nea los elementos de la fila i−ésima de <m>
for (int j = 0; j < DIM; ++j) {
// Presenta el elemento j−ésimo de la fila i−ésima de <m>
cout << setw(ANCHO) << m[i][j];
}
cout << endl;
}
cout << endl;
}
/∗
∗ Pre: DIM > 0 y <m> es una matriz cuadrada de dimension DIMxDIM
∗ Post: la matriz <m> es igual a la transpuesta de su valor inicial
∗/
void transponer (int m[[DIM]][DIM]) {
// Recorre los elementos de la matriz <m> situados por encima de su diagonal principal
// y los permuta con sus elementos simétricos, respecto de dicha diagonal
for (int i = 0; i < DIM − 1; ++i) {
for (int j = i + 1; j < DIM; ++j) {
// Permuta los elementos m[i][j] y m[j ][ i ]
int aux = m[i][j ];
m[i ][ j ] = m[j][ i ];
m[j ][ i ] = aux;
}
}
}
/∗
∗ Programa que ilustra el trabajo con matrices bidimensionales de datos enteros:
∗ − creación de una matriz,
∗ − recorrido de una matriz para mostrar sus elementos y
∗ − modificación de los elementos de la matriz al transponerla
∗/
int main () {
int mat [DIM][DIM]; // Define <mat>, una matriz DIMxDIM de datos enteros
definirValores (mat); // Asigna valor a los elementos de la matriz <mat>
mostrarValores(mat); // Muestra por pantalla los elementos de la matriz <mat>
transponer(mat); // Transpone lo elementos de la matriz <mat>
mostrarValores(mat); // Muestra por pantalla los elementos de la matriz <mat>
return 0; // El programa termina normalmente
}
103
Capı́tulo 10
En este capı́tulo se comienza repasando cómo trabajar con datos que representan caracteres.
Seguidamente se estudia cómo representar secuencias de caracteres y cómo trabajar con ellas.
El lenguaje C++ dispone de los tipos de datos elementales char y wchar t para representar
caracteres. El tipo char permite representar 128 caracteres (27 ), mientras que el tipo wchar t
permite representar un tipo más amplio de caracteres (wide char ). Pese a sus limitaciones, en
este curso trabajaremos con el tipo char para representar caracteres.
El tipo char hace uso de la representación ASCII (American Standard Code for Information
Interchange) de caracteres. La tabla que se muestra a continación muestra qué caracteres son
representados por el tipo char y cuál es el código numérico asociado a cada uno de ellos. Entre
los caracteres representados están las letras mayúsculas y las letras minúsculas del alfabeto
inglés, los diez dı́gitos, diferentes caracteres que denotan separadores, finalizadores y operadores
y diferentes caracteres no imprimibles que son representados por su abreviatura en mayúsculas
(NUL, SOH, STX, etc.). Caracteres como las letras ~ n, ~
N, ç o Ç no están representados. Tampoco lo
a, ^
están las vocales con tilde (á, Á, à, À, ^ A, ü, Ü, etc.).
Es importante saber que las letras mayúsculas tienen códigos numéricos consecutivos. Lo
mismo ocurre con las letras minúsculas o con los caracteres que representan los diez dı́gitos. Esta
propiedad hay que conocerla ya que se hace uso de ella con frecuencia a tratar con caracteres.
También conviene advertir que no es necesario conocer los códigos numéricos ASCII de cada
uno de los caracteres ya que nunca se debe hacer uso de dicho conocimiento al diseñar un
programa. Nuestros programas no serı́an portables ni generales si su código dependiera de
decisiones a nivel de implementación.
104
Código Carácter Cód. Caráct. Cód. Caráct. Cód. Caráct.
0 NUL 32 ’ ’ 64 @ 96 ‘
1 SOH 33 ! 65 A 97 a
2 STX 34 " 66 B 98 b
3 ETX 35 # 67 C 99 c
4 EOT 36 $ 68 D 100 d
5 ENQ 37 % 69 E 101 e
6 ACK 38 & 70 F 102 f
7 BEL 39 ’ 71 G 103 g
8 BS 40 ( 72 H 104 h
9 HT 41 ) 72 I 105 i
10 LF 42 * 74 J 106 j
11 VT 43 + 75 K 107 k
12 FF 44 , 76 L 108 l
13 CR 45 - 77 M 109 m
14 SO 46 . 78 N 110 n
15 SI 47 / 79 O 111 o
16 DLE 48 0 80 P 112 p
17 DC1 49 1 81 Q 113 q
18 DC2 50 2 82 R 114 r
19 DC3 51 3 83 S 115 s
20 DC4 52 4 84 T 116 t
21 NAK 53 5 85 U 117 u
22 SYN 54 6 86 V 118 v
23 ETB 55 7 87 W 119 w
24 CAN 56 8 88 X 120 x
25 EM 57 9 89 Y 121 y
26 SUB 58 : 90 Z 122 z
27 ESC 59 ; 91 [ 123 {
28 FS 60 < 92 \ 124 |
29 GS 61 = 93 ] 125 }
30 RS 62 > 94 ^ 126 ~
31 US 63 ? 95 _ 127 DEL
/∗
∗ Pre: −−−
∗ Post: Devuelve <true> si y solo si <c> es un carácter que representa una letra mayúscula
∗/
bool esMayuscula (char c) {
return c >= ’A’ && c<= ’Z’;
}
...
105
...
/∗
∗ Pre: −−−
∗ Post: Devuelve <true> si y solo si <c> es un carácter que representa una letra minúscula
∗/
bool esMinuscula (char c) {
return c >= ’a’ && c <= ’z’;
}
/∗
∗ Pre: −−−
∗ Post: Devuelve <true> si y solo si <c> es un carácter que representa un dı́gito
∗/
bool esDigito (char c) {
return c >= ’0’ && c <= ’9’;
}
Deducir el valor numérico de un carácter que representa un dı́gito es también muy simple si
se hace uso de la operaciones aritméticas aplicables a los códigos numéricos de los datos de tipo.
/∗
∗ Pre: −−−
∗ Post: si <c> es un carácter que representa un dı́gito entonces devuelve el valor numérico
∗ comprendido entre 0 y 9 representado por <c>; en otro caso devuelve un valor negativo
∗/
int valorDigito (char c) {
if (esDigito(c)) {
return c − ’0’;
}
else {
const int RESULTADO NEGATIVO = −1;
return RESULTADO NEGATIVO;
}
}
También es sencillo deducir la posición que ocupa en el alfaberto (inglés) una letra.
/∗
∗ Pre: −−−
∗ Post: si <c> es una letra, mayúscula o minúscula, entonces devuelve la posición
∗ que ocupa en el alfabeto ; si no lo es, entonces devuelve un valor negativo
∗/
int posicionAlfabeto (char c) {
if (esMayuscula(c)) {
return c − ’A’ + 1;
}
else if (esMinuscula(c)) {
return c − ’a’ + 1;
}
else {
106
const int RESULTADO NEGATIVO = −1;
return RESULTADO NEGATIVO;
}
}
97 a
98 b
99 c
100 d
...
120 x
121 y
122 z
Una cadena o secuencia de caracteres es una sucesión de cero o más caracteres. Ejemplos:
Una cadena integrada por un único caracter. Por ejemplo: "a", "7", "+", ";", etc.
Una cadena integrada por varios caracteres. Por ejemplo: "pi es 3.1416", "Manuel",
"La casa es muy luminosa", "Blanco Ruiz", "2 y dos son 4", etc.
Hay dos alternativas básicas para representar cadenas o secuencias de caracteres en C++.
107
Capítulo 11
Este capítulo presenta la noción de registro, como mecanismo de agrupación de datos relacionados
entre sí, e ilustra mediante una amplio repertorio de ejemplos cómo trabajar cuando se define un nuevo
tipo de dato con estructura de registro.
11.1. Registros
En capítulos anteriores hemos trabajado con datos con estructura de tabla (vectores y matrices),
datos agregados del mismo tipo a los que se accede a través de uno o más índices numéricos.
En este capítulo vamos a presentar un nuevo mecanismo de agregación de información que per-
mite agrupar datos relacionados entre sí que pueden ser de igual o de diferente naturaleza. Los datos
resultantes de esta agregación se denominan registros o tuplas de datos.
Un tipo de dato agregado como registro o tupla de datos se define de acuerdo con la siguiente
sintaxis:
/*
* Definición de un nuevo tipo de dato con estructura de registro
*/
struct NombreTipo {
tipoCampo1 nombreCampo1; // Primer campo del registro
tipoCampo2 nombreCampo2; // Segundo campo del registro
tipoCampo3 nombreCampo3; // Tercer campo del registro
... // Restantes campos del registro
} ;
/*
* Ya se pueden definir datos, simples o estructurados, del tipo definido anteriormente
*/
NombreTipo reg1, reg2; // reg1 y reg2 son registros de tipo NombreTipo
NombreTipo reg3; // reg3 es también un registro de tipo NombreTipo
NombreTipo T[100]; // T es un vector que indexa 100 datos de tipo NombreTipo
El tipo de dato se identifica mediante un nombre (NombreTipo). Este nombre se utiliza posteriormente
para declarar datos de ese tipo. Un registro agrupa tantos datos como sea necesario. Cada uno de ellos
1
se suele denominar campo del registro o, simplemente, campo. Cada campo de un registro se declara
especificando el tipo de dato asociado (tipoCampo1, tipoCampo1, etc.) y el nombre que el programador
da al campo (nombreCampo1, nombreCampo1, etc.).
Para trabajar de forma individualizada con cada uno de los campos de un registro se ha de seleccionar
el nombre del campo mediante el operador de selección «.» al que precede el nombre del registro y al
que sigue el nombre del campo. Ejemplos:
reg1.nombreCampo3
reg2.nombreCampo1
T[i+1].nombreCampo2
En los ejemplos que se presentan en el resto del capítulo se ilustra repetidamente el uso del operador
de selección de campo.
Cuando se define un nuevo tipo de dato puede ser recomendable diseñar una colección de funciones
básicas para facilitar el trabajo con los datos del nuevo tipo.
Desde un punto de vista metodológico conviene programar la definición de cada nuevo tipo y sus
funciones básicas en un nuevo módulo. De esta forma se facilita la reutilización del nuevo tipo en
todos los programas en los que sea necesario. La definición del nuevo módulo constará, como en otras
ocasiones, de dos ficheros:
Fichero de interfaz del módulo. Contiene la definición del nuevo tipo de dato y las declaraciones
de las funciones básicas de carácter público que se ponen a disposición de los desarrolladores de
otros módulos o programas.
Fichero de implementación del módulo con el código de sus funciones básicas indicadas en el
fichero de interfaz y, en su caso, con la definición de los elementos privados auxiliares que fueran
necesarios.
A continuación se presentan los ficheros de interfaz e implementación del módulo nif en el que se
define el tipo Nif y un conjunto de funciones básicas para trabajar con los datos de ese tipo.
Para representar el número de identificación fiscal (NIF) de una persona se ha definido el tipo de datos
Nif como un registro con dos campos. El campo dni corresponde a un entero positivo que representa
el número del documento nacional de identidad de la persona y el campo letra que representa la letra
mayúscula asociada, por motivos de seguridad, al número de DNI anterior.
2
Observar que la definición de la estructura del tipo Nif viene precedida por un comentario que
explica qué es lo que representa un dato de este tipo y, cada uno de sus dos campos, viene acompañado
por un comentario que precisa el significado de la información almacenada en él.
Siempre que se defina un nuevo tipo de dato debe procederse de un modo similar, documentando
adecuadamente el tipo definido y explicando los detalles de su representación interna.
En el caso del tipo Nif se ha optado por definir el fichero de interfaz dos funciones básicas que
pueden facilitar el trabajo con el nuevo tipo de datos:
La función esValido(unNif) permite comprobar si la letra del dato unNif es la que corresponde
a su número de DNI.
La función mostrar(unNifAEscribir) que permite escribir el dato |unNifAEscribir| en la panta-
lla, con un formato como «01234567-L».
La función calcularLetra(dni), que aunque no trabaja directamente con ningún dato de tipo
unNif, permite calcular la letra correspondiente a un número de DNI.
/*
* Fichero de interfaz nif.hpp del módulo <nif>
*/
/*
* Definición del tipo de dato Nif que representa la información del NIF
* (Número de Identificación Fiscal) de una persona.
*/
struct Nif {
unsigned dni; // número del DNI de la persona
char letra; // letra asociada al número de DNI anterior
};
/*
* Pre: ---
* Post: Ha devuelto la letra del número de identificación fiscal que corresponde
* a un número de documento nacional de identidad igual a «dni».
*/
char calcularLetra(const unsigned dni);
/*
* Pre: ---
* Post: Ha devuelto «true» si y solo si «nifAValidar» define un NIF válido, es
* decir, su letra es la que le corresponde a su DNI.
*/
bool esValido(const Nif nifAValidar);
/*
* Pre: El valor del parámetro «nifAEscribir» representa un NIF válido.
* Post: Ha escrito «nifAEscribir» en pantalla, con un formato como «01234567-L».
*/
void mostrar(const Nif nifAEscribir);
3
de implementación, invisibles desde el exterior del módulo.
/*
* Fichero de implementación nif.cpp del módulo nif
*/
#include "nif.hpp"
#include <iostream>
#include <iomanip>
#include <cctype>
using namespace std;
/*
* Pre: ---
* Post: Ha devuelto la letra del número de identificación fiscal que corresponde
* a un número de documento nacional de identidad igual a «dni».
*/
char calcularLetra(const unsigned dni) {
const unsigned NUM_LETRAS = 23;
const string TABLA_NIF = "TRWAGMYFPDXBNJZSQVHLCKE";
return TABLA_NIF.at(dni % NUM_LETRAS);
}
/*
* Pre: ---
* Post: Ha devuelto «true» si y solo si «nifAValidar» define un NIF válido, es
* decir, su letra es la que le corresponde a su DNI.
*/
bool esValido(const Nif nifAValidar) {
return calcularLetra(nifAValidar.dni) == toupper(nifAValidar.letra);
}
/*
* Pre: El valor del parámetro «nifAEscribir» representa un NIF válido.
* Post: Ha escrito «nifAEscribir» en pantalla, con un formato como «01234567-L».
* También ha modificado el carácter de relleno que utiliza el manipulador
* «setw», estableciendo el espacio en blanco.
*/
void mostrar(const Nif nifAEscribir) {
cout << setfill('0');
cout << setw(8) << nifAEscribir.dni << "-" << nifAEscribir.letra;
cout << setfill(' ');
}
4
Ejemplo de utilización del módulo nif
Se presenta a continuación un programa que, a modo de ejemplo, hace uso de los recursos definidos
en el módulo nif:
#include <iostream>
#include <cctype>
#include "nif.hpp"
using namespace std;
/*
* Programa de ejemplo de uso de los recursos definidos en el módulo «nif».
*/
int main() {
Nif nifUsuario;
if (esValido(nifUsuario)) {
cout << "El NIF ";
mostrar(nifUsuario);
cout << " es válido" << endl;
return 0;
}
else {
cout << "El NIF no es válido." << endl;
return 1;
}
}
5
11.2.2. Representación de una fecha
Para representar una fecha del calendario se ha definido el tipo de datos Fecha como un registro
con tres campos enteros de tipo unsigned que definen los valores del día, el mes y el año de la fecha
considerada.
De entre las muchas funciones que podrían proporcionarse para trabajar con datos de tipo Fecha,
se ha optado por proporcionar dos: mostrar(fecha) y esAnterior(f1, f2).
/*
* Fichero de interfaz fecha.hpp del módulo <fecha>
*/
/*
* Definición del tipo de dato Fecha
*/
struct Fecha {
unsigned dia, mes, agno;
};
/*
* Pre: ---
* Post: Ha mostrado la fecha «f» en la pantalla.
*/
void mostrar(const Fecha f);
/*
* Pre: Los valores de los parámetros «f1» y «f2» representan fechas válidas
* del calendario gregoriano.
* Post: Ha devuelto true si y solo si la fecha representada por el valor
* del parámetro «f1» es cronológicamente anterior a la representada por
* «f2».
*/
bool esAnterior(const Fecha f1, const Fecha f2);
6
Fichero de implementación del módulo fecha
/*
* Pre: ---
* Post: Ha mostrado la fecha «f» en la pantalla.
*/
void mostrar(const Fecha f) {
cout << f.dia << "-" << f.mes << "-" << f.agno;
}
/*
* Pre: «f» representa una fecha válida del calendario gregoriano.
* Post: Ha devuelto un entero que, al ser escrito en base 10, tiene un formato de ocho dígitos
* «aaaammdd» que representa la fecha «dia/mes/agno» donde los dígitos «aaaa» representan
* el año de la fecha, los dígitos «mm», el mes y los dígitos «dd», el día.
*/
unsigned componer(const Fecha f) {
return f.agno * 10000 + f.mes * 100 + f.dia;
}
/*
* Pre: Los valores de los parámetros «f1» y «f2» representan fechas válidas del calendario
* gregoriano.
* Post: Ha devuelto true si y solo si la fecha representada por el valor del parámetro «f1» es
* cronológiamente anterior a la representada por «f2».
*/
bool esAnterior(const Fecha f1, const Fecha f2) {
return componer(f1) < componer(f2);
}
Para representar la información de una persona, se ha definido el tipo de dato Persona como un
registro que agrupa los siguientes datos: su nombre y apellidos, su número de identificación fiscal, su
fecha de nacimiento y su estado civil (reducido a los estados de soltero o casado).
7
Para trabajar con datos de tipo Persona se ha optado por definir tres funciones básicas:
La función nombreCompleto(p) que facilita el nombre completo (nombre y apellidos) de una per-
sona p
/*
* Fichero de interfaz persona.hpp del módulo <persona>
*/
#include <string>
#include "nif.hpp"
#include "fecha.hpp"
using namespace std;
/*
* Definición del tipo de dato Persona que representa la información relevante
* de una persona: nombre y apellidos, número de identificación fiscal, fecha
* de nacimiento y estado civil y sexo
*/
struct Persona {
string nombre, apellidos;
Nif nif;
Fecha nacimiento;
bool estaCasado;
bool esMujer;
};
/*
* Pre: ---
* Post: Ha devuelto una cadena que representa el nombre completo de la persona «p».
*/
string nombreCompleto(const Persona p);
/*
* Pre: ---
* Post: Ha mostrado los datos de la persona «p» en la pantalla.
*/
void mostrar(const Persona p);
/*
* Pre: ---
* Post: Ha devuelto «true» si y solo si la fecha de nacimiento de «persona1»
* es estrictamente anterior a la fecha de nacimiento de «persona2».
*/
bool esMayorQue(const Persona persona1, const Persona persona2);
8
Fichero de implementación del módulo persona
#include <iostream>
#include "persona.hpp"
using namespace std;
/*
* Pre: ---
* Post: Ha devuelto una cadena que representa el nombre completo de la persona «p».
*/
string nombreCompleto(const Persona p) {
return p.nombre + " " + p.apellidos;
}
/*
* Pre: ---
* Post: Ha mostrado los datos de la persona «p» en la pantalla.
*/
void mostrar(const Persona p) {
cout << "Persona: " << nombreCompleto(p) << endl;
cout << "NIF: "; mostrar(p.nif); cout << endl;
cout << "Nacido/a el "; mostrar(p.nacimiento); cout << endl;
if (p.estaCasado) {
cout << "Casado/a" << endl;
}
else {
cout << "Soltero/a" << endl;
}
}
/*
* Pre: ---
* Post: Ha devuelto «true» si y solo si la fecha de nacimiento de «persona1»
* es estrictamente anterior a la fecha de nacimiento de «persona2».
*/
bool esMayorQue(const Persona persona1, const Persona persona2) {
return esAnterior(persona1.nacimiento, persona2.nacimiento);
}
9
Capítulo 12
En este capítulo se presentan algunos de los algoritmos básicos para resolver problemas clave en
el tratamiento de estructuras de datos indexadas. Estos algoritmos deben formar parte de los conoci-
mientos básicos de programación de cualquier estudiante universitario de Ingeniería Informática. Debe
conocerlos por su nombre, debe saber programarlos y debe saber utilizarlos adecuadamente.
Estos son algunos de los problemas de tratamiento de información a los que se enfrenta con mayor
frecuencia un programador cuando trabaja con estructuras de datos indexadas:
1
tipos de algoritmos, independiente del tipo de dato de los elementos que integran la estructura indexada
con la que trabajan. Posteriormente se ilustrará cada uno de los esquemas algorítmicos con uno o más
ejemplos de aplicación a problemas concretos.
En la presentación de cada uno de los esquemas algorítmicos se va a trabajar con vectores de ele-
mentos de un tipo genérico que denominaremos Dato. Estos esquemas son generales y son válidos tanto
para trabajar con estructuras indexadas de registros como de elementos de tipos básicos (int, double,
char, bool, etc.).
/*
* Definición de un tipo de dato genérico Dato sobre el cual se van a
* plantear los esquema algorítmicos que se presentan en este capítulo
*/
struct Dato {
tipoCampo1 c1; // campo 1º del registro
tipoCampo2 c2; // campo 2º del registro
...
tipoCampok ck; // campo k-ésimo del registro
};
Los ejemplos concretos de algoritmos ilustrativos que van a ser presentados se aplicarán a vectores
cuyos elementos sean datos de tipo Persona, presentados en el capítulo anterior.
/*
* Definición del tipo de dato Persona que representa la información relevante
* de una persona: nombre y apellidos, número de identificación fiscal, fecha
* de nacimiento y estado civil
*/
struct Persona {
string nombre, apellidos;
Nif nif;
Fecha nacimiento;
bool estaCasada;
};
Los algoritmos de recorrido de una estructura indexada resuelven problemas que exigen un trata-
miento individualizado de cada uno los elementos de la estructura.
En primer lugar se presenta un esquema general válido para cualquier algoritmo de recorrido. A con-
tinuación se ilustra su aplicación a la resolución de tres problemas concretos que requieren el desarrollo
de algoritmos de recorrido.
En los esquemas que se van a presentar en este capítulo se omiten ciertos fragmentos de código que
dependen de cada algoritmo concreto y se sustituyen por una frase explicativa de la misión de cada
fragmento, escrita entre corchetes.
Un algoritmo de recorrido de una estructura de datos indexada responde al siguiente esquema ge-
neral.
2
/*
* Pre: ---
* Post: Se han tratado todos los elementos de T[0..n-1]
*/
void tratar (const Dato T[], const unsigned n) {
[Acciones previas al tratamiento de los elementos de T[0..n-1]]
for (unsigned i = 0; i < n; ++i) {
// Se han tratado los elementos de T[0..i-1]
[Trata ahora el elemento T[i]]
// Se han tratado los elementos de T[0..i]
}
// Se han tratado los elementos de T[0..n-1]
[Acciones posteriores al tratamiento de los elementos de T[0..n-1]]
}
La función mostrar(T, n) presenta por pantalla un listado de los elementos del vector T cuyos
índices son los comprendidos en el intervalo [0, n – 1].
/*
* Pre: «T» tiene al menos «n» componentes.
* Post: Escribe en la pantalla un listado de la información de las personas de las
* primeras «n» componentes del vector «T», a razón de una persona por línea.
*/
void mostrar(const Persona T[], const unsigned n) {
for (unsigned i = 0; i < n; i++) {
// Se han mostrado las personas de las primeras i-1 componentes de «T»
mostrar(T[i]);
cout << endl;
// Se han mostrado las personas de las primeras «i» componentes de «T»
}
}
La función numCasados(T, n) devuelve el número de elementos del vector T cuyos índices están
comprendidos en el intervalo [0, n – 1] y que corresponden a personas cuyo estado civil es casado.
/*
* Pre: «T» tiene al menos «n» componentes.
* Post: Devuelve el número de casados de las primeras «n» componentes del vector «T».
*/
unsigned numCasados(const Persona T[], const unsigned n) {
/* Aún no se ha identificado ningún soltero. */
unsigned cuenta = 0;
for (unsigned i = 0; i < n; i++) {
/* cuenta == nº de casados de las primeras «i» - 1 componentes de «T» */
if (!T[i].estaCasada) {
cuenta = cuenta + 1;
}
/* cuenta == nº de casados de las primeras «i» componentes de «T» */
3
}
/* cuenta == nº de casados de las primeras «n» componentes de «T» */
return cuenta;
}
La función masEdad(T, n) devuelve un dato de tipo Persona que describe a la persona de más edad
entre los almacenados en el vector T con índices comprendidos en el intervalo [0, n – 1].
/*
* Pre: n > 0 y «T» tiene al menos «n» componentes.
* Post: Devuelve la persona de más edad de entre las primeras «n» componentes del
* vector «T».
*/
Persona masEdad(const Persona T[], const unsigned n) {
// indMayor == índice de la persona de más edad;
// inicialmente: primera componente del vector «T»
unsigned indMayor = 0;
for (unsigned i = 1; i < n; i++) {
// indMayor == índice de la persona de más edad de entre las primeras
// «i» - 1 componentes del vector «T»
if (esMayorQue(T[i], T[indMayor])) {
indMayor = i;
}
// indMayor == índice de la persona de más edad de entre las primeras
// «i» componentes del vector «T»
}
// indMayor == índice de la persona de más edad en las primeras «n»
// componentes de «T»
return T[indMayor];
}
Los algoritmos de búsqueda en una estructura indexada resuelven problemas que exigen el análisis
de, al menos, una parte de los datos de la estructura. En función de las circunstancias se podrán aplicar
diferentes algoritmos de búsqueda.
Algoritmos de búsqueda binaria en el caso de que los elementos sobre los que se plantea la
búsqueda estén ordenados respecto del criterio de búsqueda. Estos métodos son más eficientes
que los que se citan a continuación. La mayor eficiencia se acentúa en el caso de que la búsqueda
se plantee sobre un número de elementos suficientemente grande.
Algoritmos de búsqueda secuencial. Son aplicables en todos los casos. En el caso de que los
elementos sobre los que se plantea la búsqueda estén ordenados sólo está justificado el uso de
estos algoritmos si el número de elementos sobre los que se plantea la búsqueda es reducido.
4
12.3.1. Esquema algorítmico de búsqueda secuencial
La estrategia de una búsqueda secuencial es revisar los sucesivos elementos del vector comenzando
por el de menor índice (aunque se podría comenzar también por el de mayor índice). La búsqueda
concluye al encontrar el primer elemento que satisface el criterio de búsqueda o, en caso negativo, al
haber explorado la totalidad del vector sin éxito.
El esquema de una búsqueda secuencial partiendo desde el índice de valor inferior se muestra a
continuación.
/*
* Pre: ---
* Post: Si entre los datos de T[0..n-1] hay uno que satisface el criterio de búsqueda entonces
* devuelve el índice de dicho elemento en el vector; si no lo hay devuelve un valor
* negativo
*/
int busqueda(const Dato T[], const unsigned n) {
// Se ha programado un algoritmo de búsqueda secuencial comenzando por el elemento T[0]
5
El esquema de una búsqueda secuencial partiendo desde el índice de valor superior se muestra a
continuación. No difiere en nada sustancial del esquema anterior, solo en algunos detalles.
/*
* Pre: ---
* Post: Si entre los datos de T[0..n-1] hay uno que satisface el criterio de búsqueda entonces
* devuelve el índice de dicho elemento en el vector; si no lo hay devuelve un valor
* negativo
*/
int busqueda(const Dato T[], const unsigned n) {
// Se ha programado un algoritmo de búsqueda secuencial comenzando por el elemento T[n-1]
// Establece el espacio inicial de búsqueda T[0..i], es decir, T[0..n-1]
int i = n - 1;
// Por el momento no se ha encontrado lo que se busca
bool encontrado = false;
while (!encontrado && i >= 0) {
// No ha habido éxito tras buscar en T[i+1..n-1]. Analiza el elemento T[i]
[Analiza el elemento T[i]]
if (satisface T[i] el criterio de búsqueda)) {
// La búsqueda debe concluir ya que T[i] es el elemento buscado
encontrado = true;
}
else {
// La búsqueda continua en el espacio de búsqueda T[0..i-1]
i = i - 1;
}
}
// Discrimina si la búsqueda ha concluido con o sin éxito
if (encontrado) {
// La búsqueda ha concluido con éxito ya que T[i] satisface el criterio de búsqueda
return i;
}
else {
// La búsqueda ha concluido sin éxito
return -1;
}
}
Una pequeña mejora de la eficiencia de un algoritmo de búsqueda secuencial se logra si está garan-
tizada la presencia dentro el espacio de búsqueda de un elemento, al menos, que satisfaga el criterio. En
tal caso el esquema algorítmico de búsqueda secuencial con garantía de éxito es el siguiente:
/*
* Pre: n > 0 y entre los datos de T[0..n-1] hay uno que satisface el criterio de búsqueda
* Post: Devuelve el índice de un elemento de T[0..n-1] que satisface el criterio de búsqueda
*/
int busqueda (const Dato T[], const unsigned n) {
// Se ha programado un algoritmo de búsqueda secuencial comenzando por el elemento T[0]
// Establece el espacio inicial de búsqueda T[i..n-1], es decir, T[0..n-1]
unsigned i = 0;
while (no satisface T[i] el criterio de búsqueda) {
// Se han descartado los elementos de T[0..i]
i = i + 1;
// El espacio de búsqueda es ahora T[i..n-1]
}
// La búsqueda ha concluido con éxito
return i;
}
6
12.3.2. Un algoritmo de búsqueda secuencial
La función buscar(T, n, dniBuscado) devuelve un valor negativo en el caso de que no haya nin-
guna persona de T[0..n-1] con el DNI dniBuscado. Si lo hubiera, devuelve un valor del intervalo
[0, n – 1] que corresponde al índice en T de una persona con el DNI dniBuscado. El diseño de la función
buscar(T, n, dniBuscado) corresponde a un esquema algorítmico de búsqueda secuencial comenzan-
do desde el elemento de índice 0.
/*
* Pre: «T» tiene al menos «n» componentes.
* Post: Si entre las personas almacenadas en las primeras «n» componentes del
* vector «T» hay uno cuyo DNI es igual a «dniBuscado», entonces ha
* devuelto el índice de dicho elemento en el vector; si no lo hay, ha
* devuelto un dato negativo.
*/
int buscar(const Persona T[], const unsigned n, const unsigned dniBuscado) {
unsigned i = 0;
bool encontrado = false;
/* Búsqueda */
while (!encontrado && i < n) {
if (T[i].nif.dni == dniBuscado) {
encontrado = true;
}
else {
i = i + 1;
}
} // encontrado || i >= n
7
12.3.3. Esquema algorítmico de búsqueda binaria
Un algoritmo de búsqueda binaria solo es aplicable si los elementos sobre los que se realiza la bús-
queda están ordenados según el criterio que guía la búsqueda.
// Define los límites inferior y superior del espacio de búsqueda, T[inf..sup]. Establece
// que el espacio de búsqueda inicial sea T[0..n-1].
unsigned inf = 0;
unsigned sup = n - 1;
// Reduce el espacio de búsqueda, T[inf..sup], hasta que se limite a un solo elemento de T
while (inf != sup) {
// Calcula el punto medio del espacio de búsqueda T[inf..sup]
int medio = (inf + sup) / 2;
// Determina en qué subespacio seguir buscando: en T[medio+1..sup] o en T[inf..medio]
if (el valor de T[medio] es inferior al del elemento que se está buscando) {
// La búsqueda continúa en el espacio de búsqueda T[medio + 1..sup]
inf = medio + 1;
}
else {
// La búsqueda continúa en el espacio de búsqueda T[inf..medio]
sup = medio;
}
}
// Discrimina si la búsqueda ha concluido con o sin éxito
if (T[inf] satisface el criterio de búsqueda) {
// La búsqueda ha concluido con éxito
return inf;
}
else {
// La búsqueda ha concluido sin éxito
return -1;
}
}
8
12.3.4. Un algoritmo de búsqueda binaria
/* Búsqueda */
// Espacio de búsqueda: T[0..n-1]
while (inf < sup) {
// Espacio de búsqueda: T[inf..sup]
unsigned medio = (inf + sup) / 2;
if (dniBuscado > T[medio].nif.dni) {
// Espacio de búsqueda: T[medio+1..sup]
inf = medio + 1;
}
else {
// Espacio de búsqueda: T[inf..medio]
sup = medio;
}
// Espacio de búsqueda: T[inf..sup]
}
// inf >= sup
// Espacio de búsqueda: T[inf]
9
12.4. Algoritmos de distribución
El problema de distribuir los elementos de un vector, T[0..n-1], con relación a una determinada
propiedad P que satisfacen k de los n elementos del vector, consiste en permutar los elementos del
vector de forma que los elementos reubicados en las k primeras posiciones, T[0..k - 1], sea los que
satisfacen la propiedad P, mientras que los restantes elementos, T[k..n - 1], sean los que no la satis-
facen.
Se propone un esquema algorítmico iterativo para la distribución distribución de los datos de T[0..n-1].
En cada iteración se logra ubicar uno o dos elementos en la parte del vector (parte inferior o superior)
que le o les corresponde. La iteración concluye cuando todos los elementos han sido ubicados en la
parte que les corresponde.
/*
* Pre: n > 0 y sea k el número de elementos de T[0..n-1] que satisfacen una propiedad P.
* Post: T[0..n - 1] es una permutación de los datos iniciales de T[0..n - 1] en la que todos
* los elementos de T[0..k - 1] satisfacen la propiedad P y ninguno de los elementos de
* T[k..n - 1] la satisface
*/
void distribuir(Dato T[], const unsigned n) {
// Se ha programado un algoritmo de distribución de los elementos de T[0..n - 1]
// atendiendo a la satisfacción de la propiedad P.
10
12.4.2. Un algoritmo de distribución
La función distribuir(T, n), cuyo código se muestra a continuación, ha sido diseñada a partir del
esquema algorítmico mostrado en el aparado anterior. Distribuye los elementos del vector T[0..n-1]
de forma que los que corresponden a personas no casadas se ubiquen en posiciones del vector cuyo
índice sea inferior al de cualquier persona casada.
Para simplificar el código se optado por diseñar la función auxiliar permutar(una, otra) que será
reutilizada posteriormente en este mismo capítulo.
/*
* Pre: una = A y otra = B
* Post: una = B y otra = A
*/
void permutar(Persona& una, Persona& otra) {
Persona aux = una;
una = otra;
otra = aux;
}
/*
* Pre: «T» tiene al menos «n» componentes.
* Post: Las primeras «n» componentes del vector «T» es una permutación de los datos iniciales
* de «T» en la que todos las personas solteras tienen un índice en el vector menor que
* cualquier persona casada.
*/
void distribuir(Persona T[], const unsigned n) {
int inf = 0;
int sup = n - 1;
// Los elementos de «T» con índices en el intervalo [inf, sup], es decir, [0, n-1]
// (o sea, todos), han de ser distribuidos
while (inf < sup) {
// Las personas de «T» en los índices en [0, inf-1] son todas solteras y las que están
// en los índices [sup + 1, n - 1] son todas casadas. Falta por distribuir los
// elementos en el intervalo [inf, sup].
if (!T[inf].estaCasada) {
// T[inf] está soltero; por lo tanto está bien situado, al principio del vector.
inf = inf + 1;
}
else if (T[sup].estaCasada) {
// T[sup] está casado; por lo tanto está bien situado al final.
sup = sup - 1;
}
else {
// T[inf] está casado y T[sup] está soltero; por ello van a ser permutados, para
// ser colocados en la parte del vector que les corresponde.
permutar(T[inf], T[sup]);
inf = inf + 1;
sup = sup - 1;
}
// Las personas de «T» en los índices en [0, inf-1] son todas solteras y las que están
// en los índices [sup + 1, n - 1] son todas casadas. Falta por distribuir los
// elementos en el intervalo [inf, sup].
}
// inf >= sup --> Las personas de «T» en las componentes de índices en [0, inf-1] son todas
// solteras y los que están en las índices [inf, n - 1] son todas casados. Por lo tanto,
// todos los elementos de las primeras «n» componentes del vector «T» han sido distribuidos.
}
11
12.5. Algoritmos de ordenación
El problema de ordenar los elementos de un vector, T[0..n-1], con relación a un determinado cri-
terio, consiste en permutar los elementos del vector para queden ordenados según dicho criterio.
Existe una variedad de algoritmos de ordenación de vectores. En este curso de programación va-
mos a presentar el algoritmo de ordenación de vectores por selección. Se ha optado por él por su
simplicidad de diseño, aunque no sea el algoritmo de ordenación de vectores más eficiente.
Un algoritmo de ordenación por selección tiene una estructura iterativa. En la iteración i-ésima se
selecciona el menor (o, en su caso, el mayor) de los datos del vector T[i..n-1] y se permuta con el
elemento T[i]. Es inmediato observar dentro del bloque de código a iterar un algoritmo de recorrido
para seleccionar el dato menor del subvector T[i..n-1].
/*
* Pre: n > 0
* Post: T[0..n-1] es una permutación de los datos iniciales de T[0..n-1] y todos ellos están
* ordenados según un criterio C
*/
void ordenacion (Dato T[], const unsigned n) {
// Se ha programado un algoritmo de ordenación de <T> por el método de selección
La función ordenar(T,n), cuyo código se muestra a continuación, ha sido diseñada a partir del
esquema algorítmico mostrado en el aparado anterior. Ordena los elementos del vector T[0..n-1] de
forma que el elemento i-ésimo corresponde a una persona cuya edad es es menor o igual que la de las
personas ubicadas en las componentes del vector indexadas por índices del intervalo [0, i – 1] y mayor o
12
igual que la de las personas ubicadas en las componentes del vector indexadas por índices del intervalo
[i + 1, n – 1].
Para simplificar el código, se optado por reutilizar la función permutar(uno, otro) presentada en un
apartado anterior y por utilizar la función esMayorQue(persona1, persona2) que compara las edades
de dos personas y que se definió en el módulo «persona» del capítulo anterior.
/*
* Pre: «T» tiene al menos «n» componentes.
* Post: El contenido de las primeras «n» componentes del vector «T» es una
* permutación del contenido inicial de «T» en la que todas ellas están
* ordenadas de forma que cada una ha nacido en una fecha igual o anterior
* a la siguiente en el vector «T».
*/
void ordenarPorEdad(Persona T[], const unsigned n) {
if (n != 0) {
// Se ha programado un algoritmo de ordenación de un vector por el método
// de selección.
13
Capítulo 13
Un programa suele necesitar datos procedentes del exterior y suele facilitar resultados al exterior.
Cuando se habla del exterior del programa hacemos referencia a dispositivos diversos:
A los ficheros o archivos almacenados en dispositivos externos (discos, memorias USB, soportes
ópticos, en la nube, etc.) de los cuales el programa puede leer datos o en los cuales puede escribir
resultados.
Las herramientas para programar operaciones de entrada o salida no están definidas dentro del
núcleo del lenguaje C++, sino en bibliotecas predefinidas y puestas a disposición del programador:
1
• Flujos de la clase istream. Un objeto de esta clase se asocia a un flujo de entrada de datos.
La clase facilita operaciones para programar la entrada (lectura) de datos del flujo.
• Flujos de la clase iostream. Un objeto de esta clase se asocia a un flujo de entrada y salida de
datos. La clase facilita operaciones para programar la entrada y salida (lectura y escritura)
de datos del flujo.
• Flujos de la clase ostream. Un objeto de esta clase se asocia a un flujo de salida de datos. La
clase facilita operaciones para programar la salida (escritura) de datos a través del flujo.
La biblioteca predefinida <fstream>. Permite al programador definir los siguientes tipos de flujos
para trabajar con ficheros o archivos:
• Flujos de la clase ifstream. Un objeto de esta clase gestiona un flujo de entrada que se asocia
a un fichero de datos. La clase facilita operaciones para programar la entrada (lectura) de
datos del flujo.
• Flujos de la clase ofstream. n objeto de esta clase gestiona un flujo de salida que se asocia
a un fichero de datos. La clase facilita operaciones para programar la salida (escritura) de
datos del flujo.
• Flujos de la clase fstream. Un objeto de esta clase gestiona un flujo de entrada y salida que
se asocia a un fichero de datos. La clase facilita operaciones para programar la entrada y
salida (lectura y escritura) de datos del flujo.
En los apartados y en los capítulos que siguen se profundiza en el uso de estas herramientas y se
explica cómo programar operaciones de entrada y salida con ellas.
La biblioteca predefinida <iostream> tiene definidos cuatro objetos, cada uno de los cuales gestiona
un flujo de datos:
cin: objeto de la clase istream que gestiona el flujo de entrada estándar (entrada de datos desde
el teclado).
cout: objeto de la clase ostream que gestiona el flujo de salida estándar (presentación de datos
en la pantalla).
cerr: objeto de la clase ostream que gestiona el flujo de salida de mensajes de error (presentación
de mensajes por pantalla).
clog: objeto de la clase ostream que gestiona el flujo de salida de mensajes de log (historial o
registro).
Al lanzar la ejecución de un programa C++ que incluya la biblioteca <iostream>, los objetos o, si se
prefiere, los flujos cin, cout y cerr son creados automáticamente.
2
13.2.2. Operaciones definidas en la clase istream para la lectura de datos
Así como los siguientes métodos y funciones que trabajan directamente con datos de tipo char, en
las que f es un objeto de la clase istream, ya sea cin o un objeto declarado de la clase ifstream (ver
sección 13.3.1):
getline(istream& f, string& cadena): extrae una secuencia caracteres del flujo de entrada
f. Se extraen caracteres hasta que se encuentra el primer carácter '\n', que también se extrae
(el carácter '\n' indica el final de una línea en un fichero de texto). La secuencia de caracteres
asignada a cadena consta de todos los caracteres extraídos del flujo, exceptuando el carácter '\n'.
Esta función está definida en la biblioteca <string>.
getline(istream& f, string& cadena, const char delimitador): extrae una secuencia ca-
racteres del flujo de entrada f. Se extraen caracteres hasta que se encuentra el primer carácter
igual a delimitador, que también se extrae del flujo. La secuencia de caracteres asignada a cadena
consta de todos los caracteres extraídos del flujo, exceptuando el carácter delimitador. Esta fun-
ción está definida en la biblioteca <string>.
Así como el siguiente método que trabaja directamente con datos de tipo char, donde f es un flujo
de la clase ostream, ya sea cout o un objeto declarado de la clase ofstream (ver sección 13.3.2):
Al escribir una secuencia de datos en un flujo suele ser necesario utilizar alguna de las siguientes
funciones, que tienen la consideración de manipuladores:
3
flush: manipulador que vacía el búfer asociado al flujo de salida.
Los flujos de salida no suelen realizan las escrituras de los datos de forma inmediata en el dispo-
sitivo al que están asociados, sino que las almacenan de forma temporal en una zona de memoria
denominada búfer (buffer en inglés) hasta que el flujo determina que la escritura física pueda
hacerse de forma eficiente.
El manipulador flush permite al programador indicar de forma explícita en el programa que
desea que los datos del búfer se transfieran al dispositivo en ese momento.
En esta asignatura, no va a ser habitual que lo necesitemos.
endl: manipulador que inserta el carácter de nueva línea, '\n', en el flujo de salida y vacía el
búfer asociado a dicho flujo.
Una propiedad esencial de los ficheros es su persistencia. La información que almacena un fichero
permanece inalterada y disponible hasta que se decide eliminarlo o modificarlo.
Los datos que necesita un programa pueden estar almacenados en uno o varios ficheros. Por otro
lado, puede interesar que algunos de los resultados de un programa se almacenen en uno o más ficheros
para que, de este modo, permanezcan accesibles después de concluir la ejecución del programa.
Desde un programa se puede leer la información almacenada en un fichero. También se pueden crear
ficheros, modificarlos y eliminarlos. En este capítulo y en los que siguen vamos a aprender a programar
todas estas operaciones.
El lenguaje C++ facilita tres clases, definidas en la biblioteca predefinida <fstream>, que dotan al
programador de herramientas para trabajar con ficheros de datos:
ifstream: clase cuyos objetos permiten gestionar un flujo de entrada asociado a un fichero y
leer sus datos.
ofstream: clase cuyos objetos permiten gestionar un flujo de salida asociado a un fichero y es-
cribir datos en él.
fstream: clase cuyos objetos permiten gestionar un flujo de entrada y salida asociado a un fichero
y leer datos almacenados en él y escribir en él nuevos datos.
En los dos subapartados que siguen se diseñan dos funciones que ilustran cómo leer y escribir datos
en un fichero.
4
13.3.1. Lectura de datos de un fichero
/*
* Pre: ---
* Post: Si «nombreFichero» define el nombre de un fichero, entonces muestra su contenido en la
* pantalla; en caso contrario, advierte del error escribiendo un mensaje en la pantalla.
*/
void mostrar(const string nombreFichero) {
ifstream f; // Declara un flujo de entrada
f.open(nombreFichero); // Le asocia el fichero «nombreFichero»
if (f.is_open()) {
char c;
while (f.get(c)) {
// Mientras se leen los datos del flujo y la última lectura es correcta
// Se procesa el último dato leído: se escribe en la pantalla
cout << c;
}
f.close(); // Disocia el flujo y el fichero externo
} else {
cerr << "No se ha podido acceder a \"" << nombreFichero << "\"" << endl;
}
}
Cada invocación f.get(c) puede finalizar con éxito, en el caso de que todavía quedaran caracteres
pendientes de leer en el flujo f, o puede hacer que f entre en un estado de error en el caso de que todos
los caracteres del flujo f ya se hubieran leído en iteraciones anteriores y, por lo tanto, no quede ninguno
más para ser leído y asignado a f.
En el primer caso, cuando se ha podido leer con éxito, la propia invocación f.get(c), por cons-
tituir la condición de iteración del bucle while, se evaluaría como true y la ejecución de la función
mostrar(nombreFichero) continuaría en el cuerpo del bucle. En el caso contrario, cuando no se haya
podido leer un nuevo carácter del flujo porque ya no queden caracteres pendientes de leer, la invocación
f.get(c) se evaluará como false y la ejecución de la función continuaría en la instrucción que sigue
al cuerpo del bucle.
El mecanismo por el que la invocación f.get(c), además de extraer un carácter del flujo f y asig-
nárselo a c, también sirve para evaluar la condición de iteración se basa en lo siguiente:
2. Por otro lado, los objetos de las clases istream y ostream tienen definido un operador para su
conversión implícita o explícita a datos de tipo bool. El valor al que se convierte depende del
5
éxito o fracaso de las operaciones que se hayan llevado a cabo anteriormente con el flujo que
se convierte. Si todas las operaciones anteriores con el flujo se han realizado sin errores, el flujo
se convierte a true. En caso contrario, si alguna ha fallado (en particular, la última), el flujo se
convierte a false.
Como hemos hecho la lectura f.get(c) en el lugar destinado a la condición de iteración de un
bucle, se produce una conversión implícita del propio flujo f devuelto por f.get(c) a bool. Si el
valor resultante es true, quiere decir que todas las operaciones realizadas con f (y, en particular,
la última lectura) se han realizado con éxito y que, por lo tanto, hay un nuevo carácter en c
que procesar. Sin embargo, si el valor resultante es false, la última lectura no se ha realizado
correctamente y ya no hay más datos que procesar.
Finalmente, debe liberarse el fichero y disociarlo del flujo que ha permitido la lectura de sus datos
ejecutando la instrucción f.close().
Los métodos definidos en la clase ifstream que han sido utilizadas en el diseño de la función
mostrar(nombreFichero) se resumen a continuación. Para comprender el significado y conocer los
detalles sobre el uso de estas funciones y de otras definidas en la clase, conviene consultar un manual
del lenguaje1 .
A continuación, se escribe en él, uno a uno, todos los caracteres del primer fichero ejecutando reite-
radamente el método copia.put(c).
Finalmente, debe liberarse el fichero del flujo que ha permitido la lectura de sus datos, instrucción
copia.close().
1
Por ejemplo: http://www.cplusplus.com/reference/fstream/ifstream/
6
/*
* Pre: ---
* Post: Si «nombreFichero» define el nombre de un fichero, copia su contenido en
* «nombreCopia»; en caso contrario o en caso de otro error, advierte del mismo
* escribiendo un mensaje en la pantalla.
*/
void copiar(const string nombreFichero, const string nombreCopia) {
ifstream original; // Declara un flujo de entrada
original.open(nombreFichero); // Lo asocia con el fichero «nombreFichero»
if (original.is_open()) {
ofstream copia; // Declara un flujo de salida
copia.open(nombreCopia); // Lo asocia con el fichero «nombreCopia»
if (copia.is_open()) {
char c;
while (original.get(c)) {
// Mientras se leen los datos del flujo y la última lectura es correcta
// Se procesa el último dato leído: se escribe en «copia»
copia.put(c);
}
copia.close(); // Disocia el flujo y el fichero externo
}
else {
cerr << "No se ha podido escribir en \"" << nombreCopia << "\"." << endl;
}
original.close(); // Disocia el flujo y el fichero externo
}
else {
cerr << "No se ha podido acceder a \"" << nombreFichero << "\"." << endl;
}
}
Los métodos definidos en la clase ofstream que han sido utilizadas en el diseño de la función
copiar(nombreFichero, nombreCopia) se resumen a continuación. Para comprender el significado
y conocer los detalles sobre el uso de estas funciones y de otras definidas en la clase, conviene consultar
un manual del lenguaje2 .
2
Por ejemplo: http://www.cplusplus.com/reference/fstream/ofstream/
7
Capítulo 14
Este capítulo se centra en la resolución de problemas que tratan información almacenada textual-
mente en un fichero o que deben generar y almacenar información textual en un fichero. Para ello se
hará uso de herramientas presentadas en el capítulo anterior.
Un fichero de texto o, simplemente, un texto está integrado por una secuencia de líneas, es decir, por
cero o más líneas. A su vez, cada línea de un texto está integrada por una secuencia de caracteres, es
decir, por cero o más caracteres.
Se van a presentar a continuación algunos ejemplos que ilustran la variedad de información que
puede almacenar un fichero de texto y qué tipo de problemas puede plantear su creación o su lectura.
Un fichero de texto puede almacenar, por ejemplo, un soneto como el que se muestra a continuación,
escrito por Lope de Vega. Un soneto es una forma poética compuesta por 14 versos endecasílabos. Los
versos se organizan en cuatro estrofas: dos cuartetos (estrofas de cuatro versos) y dos tercetos (estrofas
de tres versos).
Entre los dos cuartetos hay una línea en blanco, es decir, una línea con cero caracteres. También hay
líneas en blanco entre el segundo cuarteto y el primer terceto y entre los dos tercetos. El texto consta
de un total de 17 líneas, 14 de ellas con un verso y las tres restantes son líneas en blanco sin ningún
carácter.
1
Un soneto me manda hacer Violante
que en mi vida me he visto en tanto aprieto;
catorce versos dicen que es soneto;
burla burlando van los tres delante.
Un fichero de texto puede almacenar una colección de datos formateados todos ellos como secuencias
de caracteres.
Cada una de las líneas del fichero amacena la información de un NIF. A partir del primer carácter
de estas líneas podemos encontrar el número del NIF y el último carácter de la línea corresponde a su
letra. El número y la letra de cada NIF se han separado con un guion (carácter ‘-’).
2
459622309-S
267341158-Z
107926793-Y
929602875-F
268241680-H
374852271-L
772895070-B
420414688-B
952582435-Z
150238453-S
102999604-S
877825604-Z
215834023-H
194750469-Q
297389667-N
845743899-C
127023920-A
856876842-C
277954284-C
549679432-V
845844484-A
571209446-H
362595201-V
701709129-H
301953415-H
906719875-Y
916157023-R
465472744-D
238345135-H
859034681-E
El programador que deba diseñar un programa que cree o lea un fichero análogo al anterior, debe
conocer con precisión cómo han de estar dispuestos los datos en el fichero. En este curso recurriremos
de forma habitual a la notación BNF para describir la sintaxis de los datos de un fichero.
3
14.2. Problemas que requieren la lectura de un texto
Se van a diseñar algunas funciones que resuelven problemas que entrañan la necesidad de leer un
fichero de texto y analizar, hasta cierto nivel, su contenido.
El objetivo de estas funciones es ilustrar cómo se deben leer los datos de un fichero de texto.
Si se invoca la función anterior sobre un fichero de texto que almacene el soneto mostrado an-
teriormente, los resultados que proporciona a través de sus dos últimos parámetros son: 17 líneas y
533 caracteres.
Cabe destacar que, tras la lectura de cada línea a través de la invocación getline(f, linea), se
actualiza el número de caracteres leídos nCaracteres en una cantidad equivalente a la longitud de la
cadena leída linea.length() más un carácter extra: el de final de línea ('\n'), que se ha extraído del
flujo f, pero no se ha copiado a la cadena linea.
4
14.2.2. Análisis del número de apariciones de cada carácter alfabético de un texto
Si se invocara la función anterior sobre un fichero de texto que contuviera El Quijote de Miguel de
Cervantes, se podría obtener el número de veces que cada letra del alfabeto inglés aparece en dicho
texto.
Para leer los diferentes datos del fichero se hace uso del operador de extracción >>, que permite ex-
traer de un flujo datos formateados de diferentes tipos. En este caso, se leen del fichero datos numéricos
(números de DNI) y caracteres (letras de NIF). También se utiliza el operador de extracción para extraer
el guion que separa el DNI de la letra del NIF. Puede observarse que dicho carácter se extrae, pero que
no se utiliza.
5
/*
* Pre: El contenido del fichero de nombre «nombreFichero» sigue la sintaxis de la regla
* <fichero-nif> y el número de NIF válidos almacenados en el fichero «nombreFichero» es
* menor o igual a la dimensión del vector «T».
* Post: Asigna a «nDatos» el número de NIF válidos del fichero y almacena en las primeras
* «nDatos» componentes del vector «T» la información de los NIF válidos almacenados en
* el fichero. A «nErroneos» le asigna el número total de NIF del fichero no válidos.
* Si el fichero se puede abrir, devuelve «true». En caso contrario, devuelve «false» y
* escribe un mensaje de error.
*/
bool leerFicheroNif(const string nombreFichero, Nif T[],
unsigned& nDatos, unsigned& nErroneos) {
ifstream f;
f.open(nombreFichero);
if (f.is_open()) {
nDatos = 0;
nErroneos = 0;
char ignorarGuion;
while (f >> T[nDatos].dni >> ignorarGuion >> T[nDatos].letra) {
// Mientras el último intento de lectura fue correcto
// Se procesan los últimos datos leídos:
if (esValido(T[nDatos])) {
nDatos++;
}
else {
nErroneos++;
}
}
f.close();
return true;
}
else {
cerr << "No se ha podido leer del fichero \"" << nombreFichero << "\""
<< endl;
return false;
}
}
6
14.3. Problemas que requieren la escritura de un texto
Para escribir los diferentes datos en el fichero se hace uso del operador de inserción <<, que permite
insertar datos de diferente tipo debidamente formateados. En este caso, datos numéricos (números de
DNI) y caracteres (guion para separar datos y la letra de cada NIF).
/*
* Pre: ---
* Post: Crea un fichero de texto de nombre «nombreFichero» en el que almacena los NIF de las
* primeras «n» componentes de «T», a razón de un NIF por línea, separando el número de
* DNI de la letra mediante un guion. Si el fichero se puede escribir devuelve «true»;
* en caso contrario, escribe un mensaje de error en «cerr» y devuelve «false».
*/
bool escribirFicheroNif(const string nombreFichero, const Nif T[],
const unsigned n) {
ofstream f;
f.open(nombreFichero);
if (f.is_open()) {
for (unsigned i = 0; i < n; i++) {
f << T[i].dni << "-" << T[i].letra << endl;
}
f.close();
return true;
}
else {
cerr << "No se ha podido escribir en el fichero \""
<< nombreFichero << "\"." << endl;
return false;
}
}