Está en la página 1de 140

Curso de Programación 1

Grado en Ingenierı́a Informática

Javier Martı́nez Rodrı́guez


Área de Lenguajes y Sistemas Informáticos
Departamento de Informática e Ingenierı́a de Sistemas

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.

1.1. Resolución automática de problemas de tratamiento de


información

La programación es una actividad cuyo fin es la resolución automática de problemas de


tratamiento de información en campos muy diversos tales como el cálculo o cómputo (del
cual procede el término computador, de origen anglosajón), la gestión (del cual procede el
término ordenador, de origen francés), el tratamiento digital de imágenes, el reconocimiento
del lenguaje natural y su traducción, el control de procesos industriales, de robots, de automóviles
o de vehı́culos espaciales, el tratamiento de señales biomédicas, etc., etc.

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.

Cuando nos enfrentamos a un problema de tratamiento de información, ¿cómo escribir


un programa de computador para resolverlo? El objetivo de este curso de programación es,
precisamente, responder esta pregunta.

El primer paso para resolver un problema de tratamiento de información, es la identificación


de uno o más métodos o procedimientos que permitan resolverlo. A continuación habrá que
seleccionar uno de ellos atendiendo a consideraciones tales como su eficiencia o su simplicidad
de diseño. El siguiente paso será la formalización, por escrito, del método seleccionado mediante
un documento en el que se haga una descripción de la información relevante asociada al problema
y del modo en que debe ser tratada para resolverlo. El resultado de esta formalización recibe el
nombre de algoritmo.

Un algoritmo puede definirse como la descripción de una secuencia ordenada y finita


de pasos, exenta de ambigüedades, cuya ejecución proporciona una solución del problema de

2
tratamiento de información considerado.

Un algoritmo puede ser escrito utilizando un lenguaje de programación de computadores


como Ada, C, C++, Cobol, Fortran, Java, Módula-2, Pascal, Python o RPG o bien utilizando
un lenguaje de los utilizados para comunicarnos en la vida ordinaria, por ejemplo, español,
inglés, francés, alemán, portugués, italiano, ruso, árabe, japonés o chino. Los primeros aportan
la ventaja de poder ser perfectamente comprendidos por los computadores, cosa que hoy en dı́a
aún no es viable para los segundos.

La descripción de un algoritmo utilizando un lenguaje de programación se denomina


programa. Existen centenares de lenguajes de programación. Su número crece cada año ya
que se diseñan nuevos lenguajes. En paralelo, algunos lenguajes caen en desuso.

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.

El resultado de este curso debe ser la adquisición de un conjunto de conocimientos y el


aprendizaje de algunas técnicas que nos permitan analizar y resolver problemas de tratamiento
de información y el desarrollo de la capacidad de escribir los algoritmos resultantes utilizando
el lenguaje de programación del que dispongamos en cada momento.

1.2. Nuestros primeros 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.

1.2.1. El código del primer programa

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
}

La primera lı́nea habilita la utilización en el programa de los recursos de la biblioteca estándar


iostream que pone a disposición del programador elementos predefinidos para programar
operaciones de entrada y de salida, es decir, para la lectura o adquisición de datos y la escritura
o presentación de resultados.

Nuestro programa incluye, a continuación, un comentario de cuatro lı́neas que describe el


comportamiento del propio programa. Este comentario comienza por el par de caracteres /* y
concluye por el par de caracteres */.

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.

Al nombre de la función le sigue una lista de parámetros encerrada entre paréntesis. En


este caso una lista sin ningún parámetro, main(). A continuación, sigue el código de la función
definida como un bloque encerrado entre un par de llaves que cuenta con un par de instrucciones
que se ejecutan en secuencia. La primera,
std::cout << " Bienvenidos a la Universidad" << std::endl;
es la que presenta por pantalla el mensaje de bienvenida.

La segunda instrucción, return 0; provoca que la función concluya su ejecución y devuelva


como resultado un valor entero igual a cero que, por convenio, denota que el programa ha
finalizado normalmente.

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.

Un espacio de nombres presente en la práctica totalidad de los programas escritos en C++ es


el denominado std. El espacio de nombres std incluye los nombres de los elementos predefinidos
en las bibliotecas estándar, tales como iostream. En principio, el nombre de estos elementos ha
de venir precedido por el nombre de su espacio de nombres, std en este caso, y seguido por el
operador :: de resolución de ámbito. Ejemplos: sdt::cout y sdt::endl

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>

using namespace std;

/∗
∗ 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;
}

1.2.2. Desarrollo y puesta punto del programa

El proceso de desarrollo de un programa escrito en C++ es muy sencillo y consta de tres


pasos: edición del texto con el código del programa, compilación del código y construcción
del programa ejecutable.

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:

1. Edición de un fichero de texto, al que daremos el nombre bienvenida.cc, que almacene


su código fuente. Para ello se puede hacer uso de cualquier programa editor de textos.

2. Compilación del fichero bienvenida.cc que contiene el código del programa,


invocando la ejecución de un programa compilador de C++, mediante la orden g++ -c
bienvenida.cc, para generar el fichero bienvenida.o con el código binario resultante
del proceso de compilación. El código binario obtenido de la compilación o traducción del
código fuente se denomina código objeto.

3. Enlace o unión de código compilado, bienvenida.o, con el código binario de otros


módulos predefinidos utilizados por el programa y construcción del programa ejecutable
o programa de aplicación, mediante la orden g++ -o saludo bienvenida.o, que crea
un programa ejecutable almacenado en el fichero saludo resultado de unir y enlazar
el código binario compilado, almacenado en bienvenida.o, con el código binario de
otros elementos predefinidos necesarios para su ejecución (por ejemplo, las funciones de
estrada/salida utilizadas de la biblioteca estándar iostream). El programa resultante ya
puede ser invocado mediante la orden ./saludo que ejecuta el código almacenado en el
fichero saludo. Como resultado de su ejecución, el programa escribirá por pantalla la frase
Bienvenidos a la Universidad.

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:

computador $ g++ -c bienvenida.cc


computador $ g++ -o saludo bienvenida.o

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 $ ...

Este proceso de edición, compilación, construcción del programa ejecutable y su ejecución


también puede gestionarse desde un entorno de desarrollo integrado (Integrated Development
Environment o IDE ). Un IDE es un programa que facilita la edición, la compilación y, en su caso,
la corrección de errores, la construcción de programas ejecutables y la ejecución de programas.

Hay un buen número de IDEs para desarrollar software en C++ como, por ejemplo, Code
Blocks, CodeLite, Dev-C++, Eclipse o NetBeans.

Cualquier código C++ ha de estar convenientemente documentado mediante la inserción


de comentarios. Permiten explicar la utilidad del programa, el significado de los datos
declarados, el comportamiento de las funciones programadas y cualquier otro detalle que facilite
la comprensión del código.

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.

Una buena documentación de un programa facilita el trabajo al propio programador que


ha de mantener el código desarrollado y facilita también la comprensión del programa a otros
programadores que deban utilizarlo o, simplemente, deseen comprender su diseño. No obstante,
conviene aclarar que una buena documentación debe ser sobria, concisa y precisa, evitando
comentarios farragosos, imprecisos o innecesarios.

1.2.3. Un programa que realiza algunos cálculos

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>

using namespace std;

/∗
∗ 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;
}

El programa consta de dos funciones, main() y datosCircunferencia(r). La función


datosCircunferencia(r) es invocada tres veces desde la función main(). En cada invocación
consta el valor que toma el parámetro r, que representa el valor del radio de una circunferencia.

El código de la función datosCircunferencia(r) consta de una declaración de un dato


constante, el dato real PI de tipo double, y de una instrucción que, al ser ejecutada, presenta
por pantalla los valores del radio de la circunferencia y de la longitud de ésta.

En la función datosCircunferencia(r) se invocan las funciones fixed, setprecision(n)


y setw(n) y en la función main() se invoca también setw(n).

La función fixed está definida en la biblioteca estándar ios, incluida en la biblioteca


estándar iostream. Su invocación provoca el cambio al modo de escritura de datos reales
en punto flotante abandonando, en su caso, la escritura de reales en notación cientı́fica. Las
funciones setprecision(n) y setw(n) están definidas en la biblioteca estándar iomanip. La
primera, setprecision(n), fija el número de decimales de los números reales que se escriban a
continuación y la segunda, setw(n), fija el número mı́nimo de carácteres que deben ocupar los
datos que se escriban a continuación, completando con caracteres de relleno en caso necesario
(espacios en blanco, por defecto).

Se ha editado el programa anterior en un fichero denominado circunferencias.cc. Podemos


compilar el código de este fichero y construir un programa ejecutable al que damos el nombre

7
circunferencias.

computador $ g++ -c circunferencias.cc


computador $ g++ -o circunferencias circunferencias.o
computador $ ...

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 $ ...

1.2.4. Un programa interactivo

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>

using namespace std;

/∗
∗ 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;
}

La principal novedad de este programa es la forma en que interacciona con el usuario.

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.

Tras editar el programa anterior en un fichero denominado circulo.cc, podemos compilarlo


y crear un programa ejecutable denominado circulo:

computador $ g++ -c circulo.cc


computador $ g++ -o circulo circulo.o
computador $ ...

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 $ ...

1.3. Especificación de algoritmos

En un proyecto de programación de mı́nima entidad podremos encontrar definidas decenas


o, incluso, centenares de funciones C++. Estas funciones colaboran en la resolución del problema
al que da respuesta el programa.

Cada función es un algoritmo que resuelve un problema concreto de tratamiento de


información. Una tarea fundamental de un programador es identificar subproblemas dentro
del problema al que se enfrenta y resolver cada uno de ellos mediante un algoritmo apropiado
(función si se programa en C++).

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.

Se distinguen dos apartados en la especificación de una funció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.

La función calcular(a,b,c,x) tiene cuatro parámetros de entrada, a, b, c y x, de tipo


double y devuelve como resultado un dato también de tipo double.

/∗
∗ 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;
}

La función escribirFecha(dia,mes,anyo) tiene tres parámetros de entrada, tres datos


enteros que definen el dı́a, el mes y el año de una fecha, y no devuelve ningún resultado ya
que se limita a presentar información por pantalla.

/∗
∗ 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;
}

La función anunciar() no tiene parámetros de entrada y no devuelve ningún resultado. Su


comportamiento se limita a presentar un mensaje por pantalla cada vez que es invocada.

/∗
∗ 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

Esta es una posible especificación de la función factorial(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

Propiedades imprescindibles de un algoritmo son:

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).

Propiedades deseables de un algoritmo, aunque no estrictamente necesarias, son:

Su generalidad por la que un algoritmo no debe limitarse a resolver un problema muy


especı́fico si con un esfuerzo suplementario asumible es capaz de resolver un problema más
general.

Su reusabilidad, es decir, debe procurarse que cada algoritmo que se diseña en el


contexto de un trabajo de programación pueda ser utilizado sin cambios o con mı́nimas
modificaciones en otros programas.

Su eficiencia, es decir, los recursos que precisa el algoritmo, especialmente su tiempo de


ejecución y la cantidad de memoria que requiere, deben ser razonables para el tipo de
problema que resuelve.

Su independencia del lenguaje de programación, del computador y del sistema operativo


de éste. El algoritmo debe poder ser expresado con sencillez en diferentes lenguajes de
programación para poder ser ejecutado en diferentes computadores que pueden estar
gestionados por diferentes sistemas operativos.

Al escribir algoritmos y programas conviene observar un conjunto de reglas de estilo cuyo


único objetivo es facilitar el trabajo del programador favoreciendo la calidad del código y su
legibilidad.

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

Un programador hace uso de lenguajes de programación para escribir sus programas. El


desarrollo, la puesta a punto y la ejecución de un programa se realizan en un computador
gestionado por su propio sistema operativo.

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.

2.1. Lenguajes de programación

Existen cientos de lenguajes de programación de computadores. Muchos de ellos son de


propósito general; entre los más difundidos en las últimas décadas podrı́amos citar Ada, C,
C++, Fortran, Java, Módula-2, Pascal o Python. Otros son de propósito especı́fico, es decir,
han sido concebidos para escribir programas que resuelvan problemas en ámbitos de aplicación
muy concretos. Ejemplos destacados son el lenguaje Cobol, creado para programar aplicaciones
de gestión, el lenguaje SQL para definir y consultar bases de datos, el lenguaje HTML para definir
páginas web o el lenguaje XML para la descripción de datos.

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.

2.1.1. Sı́mbolos de un lenguaje

Un programa es un texto integrado por una secuencia de caracteres que, agrupados,


forman secuencias de sı́mbolos que pueden pertenecer a una de las siguientes categorı́as:

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:

• Consta de uno o más caracteres.


• Debe comenzar por una letra mayúscula, una letra minúscula o el carácter "_".
Ejemplos: i, x, Y, Alumno, cuenta, valor, suma
• Los restantes caracteres, si los tiene, pueden ser letras mayúsculas, letras minúsculas,
dı́gitos o el carácter "_". Ejemplos: x, suma cifras, Grafica37, contarDatos,
codAlarma25
• Se distinguen las letras mayúsculas de las minúsculas. Ası́, son diferentes los
identificadores cuenta y Cuenta y también lo son codAlarma25 y codALARMA25.
• Un identificador no puede ser idéntico a ninguna de las palabras clave o reservadas
del lenguaje que se presentan un poco más adelante.

Para facilitar la comprensión de los programas y hacerlos más legibles se recomienda


respetar las siguientes convenciones:

• 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

La elección de identificadores para nombrar los diferentes elementos de un programa debe


hacerse procurando que cada identificador proporcione el máximo de información sobre el
elemento asociado. Con ello se facilita enormemente la legibilidad del programa.

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

Operadores: definen operaciones entre datos y se representan mediante secuencias de uno


o más caracteres. En un programa C++ podremos encontrar operadores de diferentes tipos:
operadores de relación (<, <=, >, >=, ==, ! =), operadores aritméticos (+, −, ∗, /, %,
++, −−), operadores lógicos (&&, ||, !), operadores de asignación (=, + =, − =, ∗ =,
/ =, % =), etc. Estos operadores serán presentados en detalle en el siguiente capı́tulo.
Una ilustración del uso de operadores en un programa se presenta en el siguiente fragmento
de código.

if (x => a && x <= b} {


y = y + 2 ∗ x + 1;
z = (y − a) ∗ (y − b);
}

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

Sı́mbolos separadores, finalizadores y delimitadores.


El lenguaje C++ permite utilizar secuencias integradas por el carácter espacio en blanco,
el carácter tabulador y el carácter de nueva lı́nea para separar sı́mbolos consecutivos y
ası́ lograr que el código sea más legible. Desempeñan funciones como separadores de
sı́mbolos
Otros caracteres o pares de caracteres desempeñan funciones como finalizadores de
declaraciones y de instrucciones o como delimitadores de una lista de datos o de
instrucciones.
Todos estos sı́mbolos se recogen en la siguiente tabla.

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: }

Constantes o literales: denotan valores constantes de naturaleza entera, real, booleana,


caracteres o cadenas de caracteres. El modo se expresar literalmente las diferentes
constantes se ilustra en la siguiente tabla.

Expresión literal de constantes


Tipos de constantes Su expresión literal Observaciones
0 expresa un dato de tipo int
Enteras -4097 expresa un dato de tipo int
103 expresa un dato de tipo int
0.0f expresa un dato de tipo float
5.452f expresa un dato de tipo float
-0.25f expresa un dato de tipo float
0.0 expresa un dato de tipo double
5.452 expresa un dato de tipoo double
-0.25 expresa un dato de tipo double
Reales
1.5E8f expresa un dato de tipo float
1.572E-12f expresa un dato de tipo float
-1.0E-6f expresa un dato de tipo float
1.5E8 expresa un dato de tipo double
1.572E-12 expresa un dato de tipo double
-1.0E-6 expresa un dato de tipo double
true valor lógico cierto
Lógicas o booleanas
false valor lógico falso
’A’ expresa el carácter A
’a’ expresa el carácter a
’+’ expresa el carácter +
’;’ expresa el carácter ;
Caracteres
’0’ expresa el carácter 0
’6’ expresa el carácter 6
’\0’ expresa el carácter nulo
’\n’ expresa el carácter de nueva lı́nea
”Buenas tardes” cadana integrada por 13 caracteres
”Hoy es 10 de mayo” cadana integrada por 17 caracteres
" " cadena con un carácter, un espacio
Cadenas de caracteres
"" cadena sin ningún carácter (cadena vacı́a)
"+" cadena con un carácter, el carácter +
"A" cadena con un carácter, el carácter A

16
2.1.2. La notación de Backus-Naur o BNF y la construcción de identificadores

La notación de Backus-Naur o notación BNF (Backus-Naur form) se emplea con


mucha frecuencia en Informática. Facilita enormemente la definición de las reglas sintácticas
que definen los lenguajes de programación. También se utiliza para describir la organización de
estructuras de datos. Comprender y saber hacer uso de esta notación es un deber de cualquier
profesional de la Informática dado que es ampliamente utilizada en libros y manuales. También
la utilizaremos en repetidas ocasiones en este curso.

La notación BNF emplea los siguientes meta-sı́mbolos para definir reglas sintácticas:

Meta-sı́mbolos de la notación BNF


Meta-sı́mbolos Significado y utilización
Para definir una regla sintáctica. El nombre de la regla precede al sı́mbolo
::=
y la definición de la regla le sigue. Ej.: <bit> ::= ”0”| ”1”
< > Para acotar el nombre de una regla sintáctica Ej.: <bit>
” ” Para acotar un carácter o una secuencia de caracteres. Ej.: ”1”
Permite definir como opciones alternativas las descritas a su izquierda y
|
derecha. Ej.: ”0”| ”1”
Lo encerrado entre un par de paréntesis aparece una vez en el elemento
( )
definido. Ej.: ( <bit> )
Lo encerrado entre un par de llaves puede aparecer cero, una o más veces
{ }
en el elemento definido. Ej.: { <bit> }
Lo encerrado entre un par de corchetes puede no aparecer o hacerlo una sola
[ ]
vez en el elemento definido. Ej.: [ <bit> ]

Se van a presentar algunas reglas sintácticas del lenguaje C++ como primera ilustración de la
utilidad de la notación BNF.

Un identificador de un programa C++ se construye de acuerdo con la siguiente regla


sintáctica expresada en notación BNF :

<identificador> ::= ( <mayúscula> | <minúscula> | "_" )


{ <mayúscula> | <minúscula> | <dı́gito> | "_" }

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:

Ejemplos de identificadores correctos en C++


contador— contador letras i ordenarTabla sumar Datos Fichero agente007

En cambio, las siguientes secuencias de caracteres no definen identificadores correctos en C++:

Ejemplos de identificadores incorrectos en C++


Ejemplos de identificadores incorrectos Razones de su incorrección
007agente $precio @direccion No comienzan por una letra, ni por "_"
nal nombre@direccion Contienen caracteres no válidos
contador-letras códigoSe~

Las palabras clave o palabras reservadas de un lenguaje de programación no pueden utilizarse


como identificadores. Tal uso podrı́a provocar ambigüedades en el programa y, por lo tanto,
generar problemas a las personas que tengan que leerlo y a los compiladores e intérpretes que
deban procesarlo.

2.1.3. Sintaxis de un lenguaje

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++.

<bloque> ::= "{" { <declaración> } { <instrucción> } "}"

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;
}

El segundo ejemplo se limita a una secuencia de dos instrucciones de cálculo y asignación de


valor, sin ningún dato declarado localmente en el bloque.

18
{
i = i + 1;
f = i ∗ f;
}

Otro ejemplo ilustrativo presenta la regla sintáctica correspondiente a la instrucción


condicional del lenguaje C++, con sus diferentes variantes.

<instrucciónIf> ::= "if" "(" <condición> ")"


( <instrucción> | <bloque> )
{ "else if" "(" <condición> ")" ( <instrucción> | <bloque> ) }
[ "else" ( <instrucción> | <bloque> ) ]

En la regla anterior se utilizan los metacaracteres paréntesis, llaves y corchetes. Lo que


encierran un par de paréntesis debe aparecer una vez en la frase descrita. Lo que encierran
un par de llaves puede aparecer cero, una o más veces en la frase descrita, como se ha explicado
anteriormente. Lo que encierran un par de corchetes puede aparecer cero o una vez en la frase
descrita, es decir, puede aparecer o puede no hacerlo.

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.

Algunos ejemplos de instrucciones condicionales sintácticamente correctas son los siguientes.

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.

Al escribir un programa se pueden comenter errores sintácticos, es decir, violaciones de las


reglas sintácticas del lenguaje. Estos errores son detectados por los compiladores o intérpretes
de los programas, que informan al programador de su ubicación y del tipo de error de que se
trata. Esta información es muy útil ya que facilita su corrección al programador.

2.1.4. Semántica de un lenguaje

La semántica de un lenguaje de programación determina el significado que tiene cada frase


para el computador y, por lo tanto, para el programador.

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.

Los errores semánticos de un programa son aquéllos que constituyen violaciones de


la semántica del programa. Algunos de ellos pueden ser detectados por los compiladores e
intérpretes al analizar el texto del programa e informan de ellos y de su situación al programador,
para que éste pueda proceder a su corrección. Otros errores semánticos no se ponen de manifiesto
hasta que el programa es ejecutado. El programador deberá probar el comportamiento en
ejecución del código del programa para identificar y, en su caso, corregir, estos errores.

2.2. Compiladores e intérpretes

El texto que describe un programa se denomina programa fuente. Un computador no puede


ejecutar directamente el código de un programa fuente. Para ejecutarlo se requiere el concurso
de un programa intérprete, de un programa compilador o de una combinación de ambos.

Ejecución interpretada. La interpretación de un programa fuente consiste en iterar tres


pasos:

1. Obtención de la siguiente instrucción que corresponda ejecutar del programa.


2. Análisis de la instrucción y determinación de las acciones a ejecutar.
3. Ejecución de las correspondientes acciones.

La ejecución de una instrucción lleva asociado un proceso de análisis de la propia


instrucción que puede ser complejo y, en ocasiones, reiterativo. Una instrucción deberá
ser analizada tantas veces cuantas veces deba ser interpretada. Si el programa cuenta con
iteraciones largas, el proceso de interpretación puede resultar bastante ineficiente.
Ejecución del programa compilado o ejecución compilada. Consiste, básicamente,
en traducir (compilar) el programa fuente obteniendo como resultado una versión
equivalente en lenguaje máquina. Esta versión se denomina programa objeto. Al
programa objeto se le añade el código de los algoritmos cuya utilización se describe en
el programa fuente y se encuentran almacenados en bibliotecas predefinidas para formar
lo que se denomina un programa ejecutable, una tarea ejecutable o una aplicación.
Esta última operación se denomina edición de uniones o, simplemente, unión de las
diferentes partes del código del programa (link en inglés). Un programa ejecutable puede
ser invocado para ser ejecutado cuantas veces se desee, sin necesidad de volver a compilarlo.

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.

2.2.1. El caso de C++

Para ejecutar un programa escrito en C++ es preciso compilarlo previamente y construir el


correspondiente programa ejecutable, tal como se explicó en el capı́tulo anterior.

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.

2. Se crea un programa binario ejecutable como resultado de añadir al programa objeto


resultante de la compilación, el código binario de las funciones predefinidas de biblioteca
(library en inglés) utilizadas por el programa.

3. Se ejecuta el programa programa binario obtenido en el paso anterior, tantas veces


como se desee.

computador $ g++ -c bienvenida.cpp


computador $ g++ -o saludo bienvenida.o
Bienvenidos a la Universidad
computador $ ./saludo
Bienvenidos a la Universidad
computador $ ./saludo
Bienvenidos a la Universidad
computador $ ...

2.2.2. El caso de Java

La compilación pura y la interpretación pura se aplican en muchos lenguajes. No obtante,


hay otros que se implementan a base de combinar ambas técnicas. Tal es el caso del lenguaje
Java.

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 ).

2. Se ejecuta de forma interpretada el programa objeto, tantas veces como se desee,


invocando a un intérprete del código de la JVM.

computador $ javac Bienvenida.java


computador $ java Bienvenida
Bienvenidos a la Universidad
computador $ java Bienvenida
Bienvenidos a la Universidad
computador $ java Bienvenida
Bienvenidos a la Universidad
computador $ ...

2.3. Computador, sistema operativo y entorno de programación

Un computador u ordenador es una máquina programable capaz de almacenar y ejecutar


programas. De forma muy simplificada podemos decir que un computador consta de:

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).

Un conjunto de dispositivos periféricos para comunicar el computador con el exterior


(teclados, pantallas, impresoras, altavoces, plotters, digitalizadores o scanners, etc.) y
potenciar la capacidad de almacenamiento de información (discos, cintas, CDs, DVDs,
memorias USB, etc.).

Un computador es una máquina programable que es gobernada por un sistema operativo


(Windows, Linux, Mac OS, Unix, Debian, Ubuntu, Solaris, etc.). El sistema operativo de un
computador es un conjunto de programas con una doble misión:

1. Facilitar la utilización del computador por parte de sus usuarios:

Acceso de los diferentes usuarios autorizados


Seguridad y protección
Gestión de ficheros
Edición, puesta a punto y ejecución de programas
Etc.

2. Controlar el funcionamiento interno de la máquina optimizando su rendimiento:

Gestión de la memoria
Control de dispositivos periféricos
Acceso a ficheros
Asignación de recursos y ordenación de tareas (scheduling)
Etc.

Para facilitar el trabajo de los programadores se han desarrollado entornos de desarrollo


integrado (Integrated Development Environment, IDE ). Un IDE es un programa o conjunto de
programas cuya misión es facilitar el desarrollo de programas utilizando un determinado lenguaje
de programación como, por ejemplo, Ada, C, C++, Fortran, Java, Módula-2, Pascal o Python.
Un entorno de programación integra un conjunto de herramientas para facilitar:

La edición de programas fuente

La compilación de programas fuente

La depuración y puesta a punto de programas

La ejecución y prueba de programas

La construcción de programas ejecutables y de bibliotecas

Algunos ejemplos de IDEs disponibles para desarrollar software fueron mencionados en el


capı́tulo anterior.

24
Capı́tulo 3

Información, datos, operaciones y


expresiones

Los programas resuelven problemas de tratamiento de información. La información asociada


a un problema se gestiona en un programa en forma de datos. En este capı́tulo se explicará
cómo definir esos datos dentro de un programa y las herramientas que los lenguajes facilitan a
los programadores para trabajar con ellos.

3.1. Tipos de datos

Un tipo de dato define un patrón para representar información y para trabajar con ella.
Cada tipo de dato tiene asociado:

Un conjunto o dominio de valores.

Una codificación binaria para la representación de sus datos en un computador.

Una sintaxis para representar literalmente sus datos en un programa.

Un conjunto de operaciones predefinidas que facilitan al programador el trabajo con


sus datos.

3.1.1. Tipos de datos elementales: representación literal, codificación binaria


y dominio de valores

En el lenguaje C++ se definen los cuatro tipos de datos elementales que se resumen en la
siguiente tabla.

Cuatro tipos de datos elementales de C++


Tipo Representa Representanción literal de algunos de sus valores
int Datos numéricos enteros −10972 − 107 − 13 0 5 17 209 8900872
double Datos numéricos reales −1. 0E34 − 32. 17 0. 0 1. 0E − 12 0. 47 102. 7265
char Caracteres ’A’ ’a’ ’W’ ’w’ ’6’ ’;’ ’\0’ ’\n’
bool Datos lógicos o booleanos true false

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.

Cuatro tipos de datos elementales de C++


Tipo sizeof (tipo) Núm. de datos representables Codificación
int 4 bytes 32
2 = 4294967296 enteros Código binario complemento a 2
double 8 bytes 264 = 18446744073709551616 reales Código binario en coma flotante
char 1 byte 27 = 128 caracteres Codigo binario ASCII
bool 1 byte Los 2 valores booleanos Codigo binario de booleanos

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.

Cuatro tipos de datos elementales de C++


Tipo sizeof (tipo) Núm. de datos representables Dominio de valores
int 4 bytes 232 enteros [−231 , 231 − 1]
double 8 bytes 264 reales Un subconjunto de los reales
char 1 byte 7
2 caracteres Los 128 caracteres codificados en ASCII
bool 1 byte Los 2 valores booleanos {true, false}

3.1.2. Tipos de datos elementales: operaciones predefinidas

Resolver un problema de tratamiento de información consiste en obtener, a partir de unos


datos iniciales, unos resultados que constituyan una solución válida del problema. Ello requiere
operar con los datos iniciales y realizar cálculos obteniendo, en su caso, algunos resultados
intermedios antes de alcanzar los resultados finales del problema.

En este apartado se van a presentar las operaciones predefinidas y puestas a disposición


del programador para trabajar con datos pertenecientes a los tipos elementales presentados
anteriormente. Más adelante se explicará cómo construir expresiones para programar cálculos.

El lenguaje C++ dispone de un amplio catálogo de operadores predefinidos. A continuacción


se presenta la práctica totalidad de ellos, agrupados según su función.

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

Los operadores aritméticos facilitan la programación de cálculos numéricos.

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.

Operadores lógicos o booleanos


nombre del operador operador sintaxis Tipo del resultado
Negación lógica o NO-lógico ! !a bool
Conjunción lógica, Y-lógico o AND-lógico && a && b bool
Disyunción lógica, O-lógico u OR-lógico || a || b 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

Los operadores de asignación permiten modificar el valor de una variable, asignándole un


nuevo valor, siempre del mismo tipo que la variable.

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

En el caso de que el resultado de evaluar expresión sea un dato de un tipo diferente al de


la variable a la que se asigna valor, entonces el compilador de C++ se encarga de la conversión
implicita, es decir, de la conversión automática de dicho resultado transformándolo en un dato
del mismo tipo que la variable. Más adelante se profundizará sobre este asunto.

3.1.3. Evaluación de expresiones

La evaluación de una expresión se realiza atendiendo a la prioridad de los operadores


presentes en ella.

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))

3.2. Datos variables (variables) y datos constantes (constantes)

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.

He aquı́ una primera colección de declaraciones de variables:

/∗
∗ 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

En el código de un programa C++ se pueden definir datos constantes asociándoles un tipo, un


nombre y un valor. Estos datos constantes los denominaremos simplemente constantes. Una
vez definido el valor de una constante, éste no puede ser modificado por ninguna instrucción
posterior.

/∗
∗ 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.

Imaginemos que la constante trigonométrica 3. 1416 se utiliza 100 veces en el código de un


programa. Si se desea modificar su valor para incrementar su precisión bastará modificar dicho
valor una sola vez, en la lı́nea en la que se ha definido.

/∗
∗ 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.

3.3. Asignación de valor a una variable

3.3.1. El operador de asignación

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;

La semántica de una asignación v = expresión; se resume en la siguiente secuencia de


pasos:

1. En primer lugar se evalúa la expresión expresión obteniendo un resultado.

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.

3. Se asigna el resultado obtenido a v, que pasa a sustituir a su valor anterior.

El ejemplo que se muestra a continuación presenta varias instrucciones de asignación de


valor. Tras declarar la constante PI con un valor igual a 3.14159265, se asigna valor inicial a las
variables que cuyo valor representa el radio de la base del cilindo y su altura. A continuación
se asigna valor a las variables que representan el área de la base del cilindro, su área lateral, su
área total y su volumen.

/∗
∗ 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

El lenguaje C++ dispone de varios operadores que combinan la operación de asignación de


valor a una variable con la realización previa de una operación binaria cuyo primer operando es
el valor de partida de la variable a la que se le va a asignar un nuevo valor. Algunos ejemplos de
estas operaciones se presentan a continuación, junto con un comentario que ilustra su semántica.

/∗
∗ 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;

Un nuevo ejemplo de operaciones de asignación va a permitir comprender el comportamiento


de los operadores de pre-incremento (++i), post-incremento (i++), pre-decremento (--i) y post-
decremento (i--).

/∗
∗ 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

3.3.2. La conversión de tipos

En una expresión es posible encontrar operaciones binarias aplicadas a un par de datos de


tipos diferentes. En tales casos el compilador de C++ fuerza la conversión implı́cita del dato
cuyo tipo es el menos general de ellos al tipo más general

En otras ocasiones es el propio programador el interesado en forzar una conversión explı́cita


de un dato de un tipo determinado obteniendo un dato equivalente o, en su caso, próximo de
otro tipo diferente.

En los apartados que siguen se explican ambas formas de conversión de tipos de datos.

3.3.3. La conversión implı́cita de tipos

Ante una operación de asignación, v = expresión, en la que el valor del resultado


proporcionado por la expresión es de un tipo diferente al de la variable a la que se asigna
valor, el compilador de C++ fuerza la conversión implı́cita del tipo de valor resultante de
evaluar la expresión al tipo de la variable.

/∗
∗ 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)

En el ejemplo que se muestra a continuación se calcula el área de un rectángulo a partir


de los valores su base, 5 unidades (una magnitud entera de tipo int), y de su altura, 3 (una
magnitud entera de tipo int). La expresión que calcula el área del rectángulo es el producto base
* altura, cuyos operandos son datos de tipo int, porporciona como resultado el dato 15 de tipo
int. La asignación de este valor a la variable areaEntera no plantea ningún problema dado que
la variable y el resultado de la expresión son del mismo tipo. En cambio, la asignación de este
mismo valor a la variable areaReal exige que el compilador realice una conversión implı́cita
del resultado de la expresión, el valor 15 de tipo int al valor real 15.0 de tipo double, antes de
asignarselo a la variable areaReal.

/∗
∗ 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

3.3.4. La conversión explı́cita de tipos

El operador tipo(expresión) provoca la conversión del resultado de evaluar expresión en


un dato de tipo tipo.

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

Como aplicación de lo anterior se presenta la función convertirAEuros(pts) que, dado un


número real de pesetas definido por el valor del parámetro pts de tipo double, informa del
cambio de esa cantidad en euros, en forma exacta y desglosando los euros y céntimos de euro
equivalentes tras realizar, en su caso, un redondeo.

36
Capı́tulo 4

Diseño de algunos programas


elementales

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.

4.1. Diseño de un primer programa

4.1.1. Especificación de su comportamiento

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.

Qué tabla desea escribir (0 para acabar) : 7


LA TABLA DEL 7
7 x 0 = 0
7 x 1 = 7
7 x 2 = 14
7 x 3 = 21
7 x 4 = 28
7 x 5 = 35
7 x 6 = 42
7 x 7 = 49
7 x 8 = 56
7 x 9 = 63
7 x 10 = 70

Qué tabla desea escribir (0 para acabar) : 0

En el diálogo anterior, el operador se ha limitado a pedir al programa que escriba la tabla


del 7, pero podrı́a haber demandado la escritura de cuantas tablas hubiera deseado.

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 interacción entre programa y operador. El programa pregunta reiteradamente al


operador qué tabla desea que sea escrita, hasta que éste le responde con un valor cero. La
gestión de este diálogo va a recaer en la función main() del programa.

Qué tabla desea escribir (0 para acabar) : 6


... Presenta la tabla de multiplicar del 6 ...
Qué tabla desea escribir (0 para acabar) : 7
... Presenta la tabla de multiplicar del 7 ...
Qué tabla desea escribir (0 para acabar) : 4
... Presenta la tabla de multiplicar del 4 ...
Qué tabla desea escribir (0 para acabar) : 0

La escritura de cada una de las tablas. La presentación de las diferentes tablas de


multiplicar va a recaer en la función presentarTabla(n) del programa.

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

En consecuencia, la estructura del programa a diseñar comprende dos funciones situadas,


desde el punto de vista de su diseño, a diferente nivel de abstracción:

En un nivel superior de abstracción, más próximo al problema general a resolver que


a los detalles sobre cómo resolverlo, se sitúa la funcion main(), responsable de gestionar
el diálogo con el operador y de ordenar que se cumplan las demandas de éste, sin reparar
en los detalles sobre cómo atenderlas.

En un nivel inferior de abstracción, más centrado en los detalles relativos a las


demandas del operador, se sitúa la funcion presentarTabla(n), responsable de presentar
por pantalla, cada vez que sea invocada, la tabla de multiplicar del número definido por
el valor de su parámetro n.

4.1.3. Desarrollo del código del programa

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>

using namespace std;

/∗
∗ 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.

Una directiva #include <biblioteca> inserta el código de la biblioteca estándar


especificada en ese punto del 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.

De la biblioteca (library ) estándar iostream se hace uso en el programa de los siguientes


objetos y funciones:

• 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.

De la biblioteca (library ) estándar iomanip se hace uso en el programa de la siguiente


función:

• 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.

El programa se completa con el código de las dos funciones presentarTabla(n) y main().

4.2. Ámbito o visibilidad y duración o vida de un elemento en


un programa

En un programa pueden definirse diferentes elementos tales como constantes, variables,


funciones y otros que serán descritos en capı́tulos o cursos posteriores.

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).

Ámbito o visibilidad de un elemento: es la parte o zona del código programa en la que


el elemento es accesible y, por lo tanto, el programador puede hacer uso de él.

Duración o vida de un elemento: es el tiempo en el que el elemento está disponible


durante la ejecución del programa.

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.

void funcion 01 (int i , double f, double c) { // A partir de aquı́ son visibles i , f y c


. . . // Aquı́ son visibles i , f y c
{
. . . // Aquı́ son visibles i , f y c
}
. . . // Aquı́ son visibles i , f y c
} // Fin de la visibilidad de i , f y c

Á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.

#include <iostream> // A partir de aquı́ son visibles los elementos definidos


// la biblioteca ”iostream”

using namespace std;

int fechaAmerica = 1492; // A partir de aquı́ es visible la variable global

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;
}

// Fin de la visibilidad de fechaAmerica, mostrarFechas() y main()

Al ser ejecutado el programa anterior, muestra por pantalla la siguente lı́nea:

America fue descubierta en 1492 y el hombre llego a la Luna en 1969

4.2.1. Espacios de nombres

La definición de un nuevo espacio de nombres (namespace) es muy simple. Basta darle un


nombre, por ejemplo esp1, y definir entre un par de llaves los elementos (constantes, variables,
funciones, etc.) que han de formar parte de este espacio.

namespace esp1 {

definición de elementos globales (constantes, variables , funciones, etc .)

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.

El primer programa presentado en este curso ilustra lo anterior.

#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>

using namespace std;

/∗
∗ Pre: −−−
∗ Post: Escribe por pantalla el mensaje ”Bienvenidos a la Universidad”
∗/
int main() {
cout << ”Bienvenidos a la Universidad” << endl;
return 0;
}

4.2.2. Enmascaramiento de nombres

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.

#include <iostream> // A partir de aquı́ son visibles los elementos definidos


// la biblioteca ”iostream”

using namespace std;

int fecha = 1492; // A partir de aquı́ es visible la variable global fecha

/∗
∗ 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;
}

// Fin de la visibilidad de la variable global fecha y de las funciones mostrarFechas() y main()

Al ser ejecutado el programa anterior, también muestra por pantalla la siguente lı́nea:

America fue descubierta en 1492 y el hombre llego a la Luna en 1969

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:

La rueda fue inventada en ?


America fue descubierta en 1492
El hombre llego a la Luna en 1969

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++.

El primer mecanismo de comunicación de información desde una función invocada hacia la


función invocante es, en su caso, a través del valor devuelto por la primera. Esta forma de
comunicación se presupone asumida y en el resto de este apartado nos vamos a centrar en
analizar mecanismos complementarios de comunicación de información entre las funciones de un
programa.

4.3.1. Comunicación de información a través de datos globales

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 intermedio. Función mostrarResultado() responsable de ordenar el cálculo de


la suma de todos los datos de un intervalo entero y de presentar por pantalla el resultado
obtenido. En este nivel no se entra a considerar cómo calcular el resultado del problema,
sino que se centra en decidir 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 main() define el valor de ambas variables globales.

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>

using namespace std;

int desde, hasta; // datos globales : extremos de un intervalo [desde,hasta]


/∗
∗ Pre: desde <= hasta
∗ Post: Devuelve la suma de los datos comprendidos en el intervalo entero [desde,hasta]
∗/
int sumarDatos () {
return (desde + hasta )∗ (hasta − desde + 1) / 2;
}

/∗
∗ 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.

El diseño de las funciones no es independiente. El diseño de sus tres funciones


depende de unos datos globales, las variables desde y hasta. En caso de modificar el
nombre de cualquiera de estas variables, renombrándolas por ejemplo con los nombres
extremoInferior y extremoSuperior, nos veremos obligados a modificar el código de las
tres funciones.
Las funciones sumarDatos() y mostrarResultado() no podrán ser reutilizadas
directamente en el diseño de otro programa dado que solo trabajan con dos variables
globales que han de llamarse necesariamente desde y hasta, lo cual es poco probable que
ocurra,

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.

Las razones anteriores desaconsejan utilizar datos globales en el diseño de un programa.


En este curso seremos estrictos y nunca haremos uso de esta técnica de comunicación
de datos entre funciones, salvo casos realmente excepcionales en los que dicho uso esté
plenamente justificado.

En la práctica totalidad de los programas que desarrollemos en este curso definiremos


parámetros para comunicar información entre funciones.

4.3.2. Parámetros por valor

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>

using namespace std;

/∗
∗ 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.

4.3.3. Parámetros por referencia

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);

Si pretendemos diseñar la función intercambiar(uno, otro), tal como se muestra a


continuación, no lograremos nuestro objetivo ya que la modificación del valor de los parámetros
uno y otro tiene un efecto exclusivamente local y no altera el valor de las variables a y 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;
}

El lenguaje C++ ofrece al programador la posibililidad de definir parámetros por referencia.


Sintácticamente se denotan escribiendo tras el tipo del parámetro el operador referencia &. Al

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.

Diseñamos de nuevo la función intercambiar(uno, otro), planteando como parámetros no


dos datos de tipo int, sino las referencias o direcciones en memoria de dos datos de tipo int.

/∗
∗ 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;
}

Los parámetros cuyo valor es la referencia o dirección en memoria de un dato se denominan


parámetros por referencia. Un parámetro transmitido por referencia permite la modificación
del valor del dato referenciado.

La dos funciones que se muestran a continuación presentan parámetros transmitidos por


referencia ya que es posible que los valores de los datos referenciados por cada uno de ellos
puedan ser modificados al ser ejecutado el código de la función. A su vez, ambas funciones
invocan la función intercambiar(uno,otro) para permutar los valores de determinados pares
de variables.

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.

<instrucción> ::= <instrucciónSimple> | <instrucciónEstructurada>

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.

5.1.1. Instrucciones simples

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 ”;”.

<instrucciónSimple> ::= ( <declaraciones> | <expresión> | <invocación>


| <devoluciónValor> ) ";"

<declaraciones> ::= [ "const" ] <nombreTipo> <datoDeclarado> { "," <datoDeclarado> }

<invocación> ::= <identificador> "(" [ <expresión> { "," <expresión> } ] ")"

<datoDeclarado> ::= <identificador> [ "=" <expresión> ]

54
<devoluciónValor> ::= "return" [ <expresión> ]

Algunos ejemplos de instrucciones simples:

const double PI = 3.1416; // declaración de la constante PI


int i = 0, j , k = 10; // declaración de las variables i , j , k
x = x ∗ y + z; // asignación del valor [x ∗ y + z] a la variable x
intercambiar(a, b); // invocación de la funcion intercambiar(a, b)
return suma / cuenta; // la función termina y devuelve el valor de [suma / cuenta]

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.

Instrucciones de declaración. Permiten declarar datos constantes y variables y, en su


caso, asignarles un valor. La declaración de constantes viene precedida por la palabra
reservada const. Ejemplos de instrucciones de declaración:

const double PI = 3.1416, E = 2.71828; // constantes PI y E


const char PRIMERA LETRA = ’A’, // comienzo del alfabeto
ULTIMA LETRA = ’Z’, // fin del alfabeto
PRIMER DIGITO = ’0’, // primer dı́gito
ULTIMO DIGITO = ’9’; // último dı́gito
int contador = 0, j , k = 100; // tres variables entras
double base, altura, diagonal; // tres variables reales
char c1, c2 = ’6’ , c3, c4, c5; // cinco variables de tipo char
bool test1, test2 , test3 = true; // tres variables booleanas

Instrucciones de expresión. A priori cualquier expresión permite construir una


instrucción de expresión.

expresión; // una instrucción de expresió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.

• Instrucciones de asignación que permiten modificar el valor de una variable,


asignándole el valor resultante de evaluar una expresión.

x = x ∗ y + z; // asigna el valor de [x ∗ y + z] a la variable x


y += 100; // asigna el valor de [y + 100] a la variable y
z = 2 ∗ z, // asigna el valor de [2 ∗ z] a la variable z

55
• Instrucciones de incremento y decremento que permiten incrementar o
decrementar en una unidad el valor de una variable.

++i; // incrementa en una unidad el valor de la variable i


j++; // incrementa en una unidad el valor de la variable j
x−−; // decrementa en una unidad el valor de la variable x
−−y; // decrementa en una unidad el valor de la variable y

• Instrucciones de salida para la escritura de datos de forma textual en el dipositivo


estándar de salida (asociado, por defecto, a la pantalla del terminal).
El objeto cout, definido en la biblioteca estándar iostream, gestiona un flujo de
salida dirigido desde el programa hacia el dipositivo estándar de salida. Este flujo
está integrado por una secuencia de caracteres. Aunque en este curso no vamos a
hacer programación orientada a objetos, podremos utilizar sin problemas objetos
predefinidos como cout.
Al flujo de salida se le pueden añadir datos mediante el operador <<. La instrucción
que se presenta a continuación evalúa las expresiones expresión1, expresión2, . . . ,
expresiónK, transforma cada uno de los resultados obtenidos en una secuencia de
caracteres e inserta esas secuencias en el flujo de salida, en el mismo orden en el que
han sido definidas las expresiones:

cout << expresión1 << expresión2 << ... << expresiónK; // una instrucción de salida

La presentación de los resultados por el dipositivo estándar de salida se produce


cuando, tras el operador <<, figura una invocación a determinadas funciones
predefinidas en la biblioteca iostream, concretamente a las funciones flush o endl.
Estas funciones, al igual que otras que inciden en el comportamiento de un flujo,
reciben el nombre de manipuladores. En la instrucción que sigue se invoca al
manipulador flush que presenta por el dipositivo estándar de salida la secuencia
de caracteres gestionada por el flujo de salida del objeto cout y vacia dicho flujo.

cout << flush;

En la instrucción que sigue se invoca al manipulador endl que inserta en el flujo


de salida el carácter de nueva lı́nea antes de presentar por el dipositivo estándar de
salida la secuencia de caracteres gestionada por el flujo de salida del objeto cout y
vaciar dicho flujo.

cout << endl;

En una misma expresión es posible mezclar operaciones de inserción de datos en el


flujo de salida con invocaciones a manipuladores de dicho fujo. Un primer ejemplo:

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

El objeto cin gestiona el flujo de entrada dirigido desde el dispositivo estándar de


entrada hacia el programa. El operador de extracción >> aplicado a una variable,
>> var, tiene como efecto la lectura de la primera secuencia de caracteres disponibles
en el flujo de entrada, su transformación en un dato del tipo que corresponda a la
variable var y la asignación del valor obtenido a dicha variable.
• Instrucción nula que no produce ningún efecto al ser ejecutada y cuya sintaxis se
limita al finalizador ";".

; // una instrucción nula

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.

intercambiar(x,y); // invoca la ejecución de la función intercambiar(x, y)


ordenar(alfa ,beta); // invoca la ejecución de la función ordenar(alfa, beta)
ordenar(uno, dos, tres ); // invoca la ejecución de la función ordenar(uno, dos, tres )

Instrucciones de devolución de valor en una función. Permiten definir, en su caso,


el valor devuelto por la función y, en cualquier caso, dan por concluida la ejecución del
código de la función.

return 0; // la función termina devolviendo el valor de [0]


return contador; // la función termina devolviendo el valor de [contador]
return 100 ∗ i + j ∗ k; // la función termina devolviendo el valor de [100 ∗ i + j ∗ k]

5.1.2. Instrucciones estructuradas

C++ permite programar cierto número de instrucciones estructuradas. En este curso no


vamos a hacer uso de todas ellas, sino que vamos a trabajar exclusivamente con las
siguientes instrucciones estructuradas: bloque secuencial de instrucciones (bloque), instrucción
condicional (instrucciónIF) y dos tipos de instrucciones iterativas (instrucciónWHILE e
instrucciónFOR).

Su sintaxis queda resumida en la siguiente colección de reglas BNF:

<instrucciónEstructurada> ::= <bloque> |


<instrucciónIF> |
<instrucciónWHILE> |
<instrucciónFOR>

<bloque> ::= "{" { <instrucción> } "}"

<instrucciónIF> ::= "if" "(" <condición> ")" <instrucción>


{ "else if" "(" <condición> ")" <instrucción> }
[ "else" <instrucción> ]

<instrucciónWHILE> ::= "while" "(" <condición> ")" <instrucción>

<instrucciónFOR> ::= "for" "(" <inicialiazación> ";" <expresión> ";"


<actualización> ")" <instrucción>

<inicialiazación> ::= <expresión>

<condición> ::= <expresión>

<actualización> ::= <expresión>

A continuación se analiza cada una de las instrucciones estructuradas presentadas.

58
Bloque secuencial de instrucciones

Un bloque de instrucciones consta de una secuencia de instrucciones escritas entre un par de


llaves.

<bloque> ::= "{" { <instrucción> } "}"

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;
}

El ámbito de visibilidad de las variables declaradas dentro de un bloque, como es el caso de


la variable aux del ejemplo anterior, se limita al propio bloque. Estas variables son creadas al
iniciarse la ejecución del bloque y desaparecen al finalizar su ejecución por lo que la memoria
que tenı́an asignada es liberada.

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

Una instrucción condicional (instrucciónIF) permite tratar un problema de formas


alternativas en función de la satisfacción de determinadas condiciones.

<instrucciónIF> ::= "if" "(" <condición> ")" <instrucción>


{ "else if" "(" <condición> ")" <instrucción> }
[ "else" <instrucción> ]

<condición> ::= <expresión>

Su ejecución obedece las siguientes reglas semánticas:

Si se satisface la condición descrita inmediatamente después de la palabra reservada if


entonces se ejecuta únicamente la instrucción (simple o estructurada) que le sigue.
En caso contrario, si hay cláusulas else if entonces se evalúan las condiciones asociadas
a cada cláusula y se ejecuta únicamente la instrucción (simple o estructurada) que sigue a
la primera de esas cláusulas cuya condición asociada se satisfaga.
Si no hay programadas cláusulas else if o, en caso de haberlas, no se satisface ninguna
de sus condiciones asociadas, entonces se ejecuta únicamente la instrucción (simple o
estructurada) descrita en la cláusula else, en el caso de que se haya programado esta
cláusula.

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

La primera de las instrucciones iterativas, instrucciónWHILE, permite iterar la ejecución


de una instrucción (simple o estructurada) mientras que una determinada condición, descrita
mediante una expresión, se satisfaga.

<instrucciónWHILE> ::= "while" "(" <condición> ")" <instrucción>

<condición> ::= <expresión>

El código a iterar puede tratarse de una instrucción simple o estructurada. El número de


veces que vaya a ser ejecutado dependerá de la satisfacción de condición, es decir, cero, una o
más veces. He aquı́ un ejemplo ilustrativo del uso de la instrucción iterativa.

/∗
∗ 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!

La segunda instrucción iterativa, instrucciónFOR, responde a la siguiente sintaxis.

<instrucciónFOR> ::= "for" "(" <inicialiazación> ";" <condición> ";"


<actualización> ")" <instrucción>

<inicialiazación> ::= <expresión>


<condición> ::= <expresión>
<actualización> ::= <expresión>

Permite programar la repetición de una instrucción (simple o estructurada) de acuerdo con


el siguiente comportamiento. Comienza ejecutando la inicialización, escrita como primer
elemento de su cabecera, y evalúa condición escrita a continuación, que debe rendir como
resultado un valor de tipo bool. Si este resultado es false la instrucción concluye, pero si
es true presenta un comportamiento repetitivo ejecutando los tres pasos que se describen a
continuación hasta que la última evaluación de condición rinda como resultado un valor false.

1. Ejecuta la instrucción (simple o estructurada) descrita tras la cabecera de la instrucción.

2. Ejecuta la instrucción de actualización descrita como último elemento de la cabecera de


la instrucciónFOR.

3. Evalúa condición, descrita como segundo elemento de la cabecera de la instrucción. Sólo


en el caso de que el resultado sea cierto (true) vuelve a iterar la ejecución de estos tres
pasos.

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

Problemas de cálculos con enteros

En este capı́tulo se ilustra la utilización de las instrucciones presentadas en el capı́tulo anterior


para resolver algunos problemas que requieren trabajar con datos enteros. Las funciones que van
a ser diseñadas tiene un alto valor pedagógico y, a la vez, un notable interés práctico dado que
magnitudas enteras están presentes en la práctica totalidad de problemas de tratamiento de
información a los que un programador se enfrenta a diario.

6.1. Cálculos con datos enteros

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.

• Operadores unarios de signo, + y -. Ejemplo: +a ∗ −b


• Operadores binarios de multiplicación, divisón entera y módulo (resto), *, /, y %.
Ejemplo: (a %b)/(c ∗ d)
• Operadores binarios de suma y resta, + y -. Ejemplo: a − b + c

64
Desbordamiento (overflow ) en un cálculo

El resultado de una operación aritmética binaria tal como a + b, a − b o a ∗ b puede producir


un resultado que caiga fuera del intervalo de enteros representables por el tipo de dato int. En
tal caso se produce un desbordamiento u overflow (en inglés) y el resultado de ejecutar la
operación es erróneo, desde un punto de vista matemático.

Consideremos el siguiente código:

int factorial = 1; // factorial = 0!


for (int i = 1; i <= 18; ++i) {
// En cada iteración escribe una lı́nea mostrando el valor de <i> y de i!
factorial = i ∗ factorial ; // factorial = i!
cout << i << ”! = ” << factorial << endl;
}

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 .

Los errores de desbordamiento deben preocupar únicamente cuando se programen cálculos


enteros cuyos resultados intermedios o finales puedan quedar fuera del intervalo de datos enteros
representables, como en el ejemplo que se acaba de mostrar.

6.2. Resolución de problemas de cálculo con enteros

En este apartado se presentan y resuelven varios problemas de tratamiento de información


entera. Se han clasificado en dos bloques, en función de la naturaleza de los problemas: problemas
que requieren el tratamiento cifra a cifra de un número entero y problemas relativos a la
divisibilidad de enteros.

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.

Al procesar información de naturaleza entera con frecuencia se plantean problemas relativos


a la imagen de un número cuando se escribe en base 10. Por ejemplo, ¿cuántas cifras tiene?
¿cuánto suman sus cifras? ¿cuál es la cifra de las decenas o la de las centenas? ¿cuál serı́a su
valor si sus cifras se escribieran en orden inverso, es decir, si las más significativas se situaran a
la derecha y las menos significativas a la izquierda? ¿es un número capicúa?

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.

Número de cifras de un entero expresado en base 10

En primer lugar se presenta la función numCifras(n) que devuelve el número de cifras de su


parámetro n, cuando el valor de éste se escribe 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

La función sumaCifras(n) devuelve el número de cifras de su parámetro n, cuando el valor


de éste se escribe 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

La función cifra(n,i) devuelve el valor de la i-ésima cifra menos significativa de su


parámetro n, cuando éste se escribe en base 10. Ası́, por ejemplo, el resultado de una
invocación cifra(6470431,1) es igual a 1, la primera cifra menos significativa, el resultado
de una invocación cifra(6470431,2) es igual a 3, la segunda cifra menos significativa, y ası́
sucesivamente.

/∗
∗ 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.

número su imagen especular


-103044 -440301
-73000 -37
-3 -3
0 0
3 3
31 13
10300 301
123456 654321

El código de la función imagen(n) es el siguiente.

/∗
∗ 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

bool negativo = n < 0;


if (n < 0) {
n = −n;
}
// Calcula la cifra más significativa de la imagen de <n>
int suImagen = n %1 0;
n = n / 10;
while (n != 0) {
// Incorpora en cada iteración una cifra a <suImagen>
suImagen = 10 ∗ suImagen + n % 10;
// Elimina la cifra menos significativa de <n>
n = n / 10;
}
// Si <n> era inicialmente negativo devuelve el valor de <suImagen>
// cambiado de signo; si era positivo devuelve el valor de <suImagen>
if (negativo) {
return −suImagen;
}
else {
return suImagen;
}
}

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

Comprobar si un entero es primo

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.

La función analiza en primer lugar si el valor de n es igual a 2. En tal caso n es primo.

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.

En caso contrario, el parámetro n es un impar mayor que 2. La función analiza si alguno



de los impares del intervalo [3, n] es divisor de n . En caso afirmativo concluye que n no es
primo. Si, tras analizar el intervalo completo, no existe ningún divisor, entonces concluye que n
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

El cálculo del máximo común divisor de un par de enteros mediante la descomposición de


ambos en factores primos no es el método más eficiente. Es mucho más eficiente aplicar el
conocido algoritmo de Euclides que se fundamenta en dos propiedades matemáticas:

a > 0 → mcd(a, 0) = a. El máximo común divisor de cualquier entero positivo a y 0 es


igual al propio entero a.

a ≥ 0 ∧ b > 0 → mcd(a, b) = mcd(b, a %b). El máximo común divisor de un par de enteros


(a,b) que satisfagan a ≥ 0 ∧ b > 0, es igual al máximo común divisor del segundo de ellos,
b, y del resto de la división entera del primero por el segundo, a%b.

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

Desarrollo modular y descendente de


programas

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ú.

7.1. Diseño de un programa dirigido por menú

Pretendemos diseñar un programa de aplicación de cierta complejidad. El programa ha de ser


interactivo (debe mantener un diálogo con el operador), estará dirigido por menú (presentará
un menú informativo al operador mostrándole sus opciones) y tendrá un comportamiento
iterativo (mantendrá el diálogo con el operador hasta que éste decida concluir dicho diálogo).

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

Seleccione una operación [0-5]: 4


Escriba un número entero : 8802361
El número imagen especular del 8802361 es el 1632088

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

Seleccione una operación [0-5]: 3


Escriba un número entero : 2703353
Seleccione la posición de una cifra : 6
La cifra situada en la posición 6 del número 2703353 es 7

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

Seleccione una operación [0-5]: 5


Escriba un número entero : 103
El número 103 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

Seleccione una operación [0-5]: 2


Escriba un número entero : 90772203
Las cifras de 90772203 suman 30

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

Seleccione una operación [0-5]: 0

74
7.2. Estructura modular y diseño descendente del programa

El desarrollo de un programa con una mı́nima entidad exige su descomposición en varias


partes, que denominaremos módulos, que puedan ser desarrolladas de forma independiente y
no necesariamente por el mismo programador.

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.

En el caso general, un programa C++ consta de varios módulos:

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);

• Implementación del módulo. En ella se incluye el código completo de las funciones


declaradas en la interfaz, ası́ como la definición de recursos privados para uso interno
del módulo (datos constantes, datos variables y funciones privadas adicionales). La
implementación de un módulo se define en un fichero cuyo sufijo será ".cc" (o también
".cpp"). A su vez, en la implementación de un módulo se puede hacer uso de recursos
definidos como públicos en otros módulos.

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:

Un módulo principal, almacenado en el fichero calculadoraEnteros.cc (o en


calculadoraEnteros.cpp), en el que se define la función main(), función principal del

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.

Un módulo de biblioteca en el cual se definen siete funciones que realizan diferentes


cálculos con números enteros y que se declaran como públicas para su utilización desde
otros módulos. Su interfaz se almacena en el fichero calculos.h y su implementación en
el fichero calculos.cc (o en calculos.cpp).

7.2.1. Diseño de un módulo de biblioteca

El fichero calculos.cc almacena la implementación del módulo de biblioteca


calculos en el cual se definen siete funciones que realizan diferentes cálculos con números
enteros: numCifras(n), sumaCifras(n), cifra(n,i), imagen(n), factorial(n), esPrimo(n)
y mcd(a,b). El contenido de este fichero es el siguiente:

/∗
∗ 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
}

El fichero de cabecera calculos.h almacena la interfaz del módulo, es decir la declaración


de los recursos del módulo calculos que se declaran públicos. En este caso, contiene la
declaración de las funciones públicas del módulo. La declaración de una función se limita a
su encabezamiento seguido del sı́mbolo finalizador "; ", tal como se ha indicado en el apartado
anterior.

/∗
∗ 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

El módulo principal ha de incluir la función principal del programa, su función main(), y ha de


presentar la directiva #include calculos.h en sus primeras lı́neas para que allı́ se inserten las
declaraciones de los elementos públicos del módulo de biblioteca calculos para su uso posterior.

En el diseño del programa se ha aplicado una metodologı́a descendente. La función main() se


sitúa al máximo nivel del abstracción, el más próximo al problema a resolver. Su diseño se apoya
en dos funciones auxiliares, presentarMenu() y ejecutarOrden(operacion), que se sitúan a
un segundo nivel de abstracción, algo más alejado del problema a resolver y algo más próximo a
los detalles de más bajo nivel. A su vez, en el diseño de la función ejecutarOrden(operacion)
se hace uso de algunos de los recursos definidos en el módulo de biblioteca calculos. Las
funciones públicas de este módulo pueden ser ubicadas en un tercer nivel de abstracción dentro
del programa, alejado del problema resuelto en este programa y centrado en los detalles sobre
cómo trabajar con datos enteros.

Un resumen de las ideas anteriores sobre el diseño descendente del programa se presenta a
continuación.

Nivel 1 (nivel superior de abstracción). Función main() responsable de la interacción con


el operador y de invocar la ejecución de las órdenes seleccionadas por éste.
Nivel 2 (segundo nivel de abstracción). En este nivel se sitúan dos funciones. La función
presentarMenu(), responsable de presentar el menú con las opnciones disponibles para
operar con datos enteros y la función ejecutarOrden(operacion), responsable de ejecutar
la operación cuyo código numérico viene definido por el valor de su parámetro.
Nivel 3 (nivel inferior de abstracción). En este nivel se sitúan las funciones públicas del
módulo de biblioteca calculos que facilitan determinados cálculos con enteros.

Un listado del código del módulo principal del programa se presenta a continuación.

// Fichero calculadoraEnteros.cc que almacena el módulo principal del programa

#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

Problemas de cálculo con números


reales

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.

8.1. Cálculos con datos 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:

El problema de desbordamiento (overflow ). Se presenta cuando el resultado de la


operación aritmética cae fuera del intervalo de valores representables por el tipo.

El problema de la falta de precisión. El tipo double, cuando es implementado con 8


bytes, permite representar "únicamente" 264 números reales. Esto supone que la práctica
totalidad de los números reales no pueden ser representados de forma exacta. Significa
que el resultado de una operación aritmética entre reales es muy posible que no pueda
ser representado exactamente, sino mediante un real representable cuyo valor sea el más
próximo al resultado, produciéndose un pequeño error por la falta de precisión.
Normalmente la falta de exactitud al realizar cálculos no es relevante ya que en cualquier
cálculo entre reales se garantiza la exactitud de un determinado número de las cifras más
significativas en el resultado. En cambio, en problemas que exigen iterar un elevado número
de cálculos con número reales y demandan una gran precisión en los resultados, el problema
puede llegar a ser relevante.

Los dos problemas anteriores quedan ilustrados al ejecutar el código que se presenta a
continuación.

double base = 923.47, x = 1.0;


for (int i = 1; i<=200; ++i) {
x = base∗x;
cout << base << ”ˆ” << i << ” = ” << x << endl;
}

Los resultados presentados al ser ejecutado el código son los siguientes:

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
... ...

Las tres últimas lı́neas ponen de manifiesto un problema de desbordamiento al calcular


923. 47104 , 923. 47105 y 923. 47106 . Sus resultados matemáticos caen fuera del intervalo de valores
reales representables como datos de tipo double.

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:

Los resultados calculados no son necesariamente exactos, son resultados aproximados


a los verdaderos resultados matemáticos.

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.

La biblioteca estándar cmath

La biblioteca estándar cmath facilita una colección de funciones matemáticas predefinidas


para programar cálculos con datos reales. Conviene consultar en un manual de C++ la lista de
funciones disponibles y sus caracterı́sticas. Un resumen con algunas de las funciones de uso más
frecuente definidas en cmath se presenta a continuación.

Función valor absoluto:

• double abs (double a) - Devuelve |a|, el valor absoluto de a

Funciones trigonométricas:

• double sin (double a) - Devuelve el valor de sen a, con a expresado en


radianes
• double cos (double a) - Devuelve el valor de cos a, con a expresado en radianes
• double tan (double a) - Devuelve el valor de tg a, con a expresado en radianes

Funciones exponencial y logarı́tmicas

• double exp (double a) - Devuelve el valor de ex


• double log (double a) - Devuelve el valor de logn x
• double log10 (double a) - Devuelve el valor de log10 x
• double log2 (double a) - Devuelve el valor de log2 x

Funciones que calculan raı́ces y potencias



• double sqrt (double a) - Devuelve el valor de x
• double pow (double x, double y) - Devuelve el valor de xy

Funciones de aproximación y redondeo a valores reales sin decimales

• 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

El código que se presenta a continuación ilustra el significado de las funciones anteriores.

/∗
∗ 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;
}

// Esta sección de código ilustra el uso de algunas de las funciones de la biblioteca


// predefinida cmath

// Funciones trigonométricas (los argumentos en radianes)


const double PI = 3.141592653;
double angulo = PI/6.0;
cout << ”sin(” << angulo << ”) = ” << sin(angulo) << ” cos(” << angulo << ”) = ”
<< cos(angulo) << ” tan(” << angulo << ”) = ” << tan(angulo) << endl;

// Funciones exponencial y logarı́tmicas ∗/


cout << ”exp(2.16) = ” << exp(2.16) << ” log(exp(2.16)) = ” << log(exp(2.16))
<< ” log10(10000) = ” << log10(10000) << ” log2(1024) = ” << log2(1024)
<< endl;

// Funciones que calculan raı́ces y potencias


cout << ”sqrt(2) = ” << sqrt(2) << ” sqrt(3) = ” << sqrt(3)
<< ” pow(2.0,10.0) = ” << pow(2.0,10.0) << endl;

// Función valor absoluto


cout << ”abs(10.903) = ” << abs(10.903) << ” abs(−10.903) = ” << abs(−10.903) << endl;

// Funciones floor(x), ceil (x), round(x) y trunc(x)


cout << endl << setw(8) << ”x” << setw(16) << ”floor(x)” << setw(10)
<< ”ceil(x)” << setw(10) << ”round(x)” << setw(10) << ”trunc(x)” << endl;
mostrarResultadosFunciones(2.49);
mostrarResultadosFunciones(2.5);
mostrarResultadosFunciones(2.501);
mostrarResultadosFunciones(−2.49);
mostrarResultadosFunciones(−2.5);
mostrarResultadosFunciones(−2.501);

86
Y este serı́a el listado con los resultados escritos por pantalla al ejecutar el código anterior.

sin(0.5235999) = 0.5 cos(0.5235999) = 0.866025 tan(0.5235999) = 0.577735


exp(2.16) = 8.67114 log(exp(2.16)) = 2.16 log10(10000) = 4 log2(1024) = 10
sqrt(2) = 1.4142 sqrt(3) = 1.73205 pow(2.0,10.0) = 1024
abs(10.903) = 10.903 abs(−10.903) = 10.903

x floor (x) ceil (x) round(x) trunc(x)


2.49 2 3 2 2
2.5 2 3 3 2
2.501 2 3 3 2
−2.49 −3 −2 −2 −2
−2.5 −3 −2 −3 −2
−2.501 −3 −2 −3 −2

8.2. Resolución de problemas que trabajan con reales

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++.

8.2.1. Cálculo de algunas magnitudes relevantes de un polı́gono regular

La función poligono(nLados,lado,perimetro,area,angulo) calcula el perı́metro, el área


y el valor de los ángulos determinados por dos lados consecutivos de un polı́gono regular. El
polı́gono regular viene definido por los parámetros nLados, número de lados del polı́gono, y lado,
longitud de cada uno de sus lados. Los tres resultados, perı́metro, área y ángulos interiores, se
asignan a las variables cuyas referencias corresponden a sus tres últimos parámetros, perimetro,
area y angulo.

/∗
∗ 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

Se va a diseñar una función de nombre ecuacionSegundoGrado(a,b,c) que, al ser invocada,


escriba por pantalla las dos raı́ces de una ecuación de segundo grado con coeficientes reales
a.x2 + b.x + c = 0. Los coeficientes a, b y c de la ecuación son parámetros de la función.

Las raı́ces de la ecuación pueden ser bien dos números reales o bien un par de números
complejos conjugados.

Las raı́ces de la ecuación x2 − 3x + 2 = 0 son dos números reales, x = 2. 0 y x = 1. 0. La


función las escribirá por pantalla del siguiente modo:

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:

Primera raı́z. Parte real: 1 Parte imaginaria: 2i


Segunda raı́z. Parte real: 1 Parte imaginaria: -2i

El código de la función ecuacionSegundoGrado(a,b,c) se muestra a continuación.

/∗
∗ 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.

Cálculo aproximado del valor de la función exponencial

El valor de la función exponencial, ex , puede calcularse de forma aproximada sumando un


número suficientemente grande de términos de la serie:
x1 x2 xn
ex = 1 + 1! + 2! + ··· + n! + ...

La función exponencial(x), que se presenta a continuación, devuelve un valor aproximado


de ex . Para ello suma todos aquellos términos de la serie cuyo valor absoluto sea mayor o igual
que el valor de la constante COTA. Conviene observar que si se reduce este valor el resultado
de la función será, en principio, más preciso. No obstante su precisión no puede ser aumentada
todo lo que desee reduciendo el valor de la constante COTA. La precisión que proporciona la
implementación del tipo double marca el lı́mite a la precisión de los resultados proporcionados
por la función.

/∗
∗ 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

Estructuración indexada de datos

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.

9.1. Estructuras de datos indexadas

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.

int T[N]; // Creación de una tabla undimensional (vector) con N elementos


double M[NF][NC]; // Creación de una tabla bidimensional (matriz) con NFxNC elementos

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.

char letrasRomanas[] = { ’I’, ’V’, ’X’, ’L’, ’C’, ’D’, ’M’ };


int valoresLetrasRomanas[] = { 1, 5, 10, 50, 100, 500, 1000 };
int matrizIdentidad [][3] = {
{ 1, 0, 0 },

93
{ 0, 1, 0 },
{ 0, 0, 1 }
};

Por analogı́a a la terminologı́a matemática, las tablas unidimesionales se suelen denominar


vectores y las tablas multidimensionales matrices.

El esquema que se presenta a continuación ilustra la estructura del vector v y de la matriz M.


Es importante conocer que los elementos de cualquier tabla o matriz se almacene en memoria
en posiciones consecutivas. De esta forma se posibilita el acceso directo a cualquier elemento a
partir de los valores de sus ı́ndices.

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. Trabajo con tablas unidimensionales

9.2.1. Selección de sus elementos y las tablas como parámetros de una función

El programa que se presenta a continuación ilustra el trabajo con tablas unidimensionales


cuyos elementos son datos enteros de tipo int. En él se crea una tabla, se asigna a sus elementos
valores enteros generados de forma pseudoaleatoria, se muestran los valores de dichos elementos
por pantalla, se procede a modificar algunos de sus elementos (anulando los que presentan un
valor negativo) y se vuelve a mostrar por pantalla los valores actualizados de los elementos de
la tabla.

El código que sigue ilustra algunas ideas que conviene conocer:

Los elementos de una tabla T se nombran de la forma T[ı́ndice] donde: T es el nombre de


la tabla, el par de corchetes define el operador de selección de sus elementos e ı́ndice
es una una expresión cuyo valor define el ı́ndice del elemento seleccionado de la tabla.
Cada operación con el conjunto de elementos de una tabla conviene programarla como una
función independiente. Con ello se facilita la reutilización de ese código a la vez que se
modulariza el programa. De este modo, en el diseño del programa anterior se ha optado por
programar las funciones definirValores(v,n), mostrarValores(v,n) y filtrar(v,n),
cada una de las cuales realiza una operación diferente con los elementos de la tabla.
Por razones de eficiencia, en C++ los parámetros de una función que representan tablas
son siempre parámetros por referencia. Para denotar la referencia a una tabla no se
utiliza el operador &, sino el nombre de la tabla seguido por un par de corchetes, tal como
se ilustra en este programa. El no tener que fijar el número de elementos de una tabla
que sea parámetro de una función, tiene como ventaja adicional que la función puede ser
invocada para tablas de diferente tamaño.
En los funciones definirValores(v,n), mostrarValores(v,n) y filtrar(v,n) el primer
parámetro es una referencia a una tabla de enteros. Se ha optado por definir un segundo

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

using namespace std;

/∗
∗ 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;
}

9.2.2. Algunos algoritmos que trabajan con tablas unidimensionales

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.

Función que determina si un número entero es primo

La función esPrimo(n) determina si un número entero menor que 100 es o no un número


primo. Resuelve el problema creando la tabla TABLA PRIMOS que almacena todos los número
primos menores que 100.

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;
}
}

Función que determina si un NIF es válido

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.

Tabla de letras de NIFs


DNI % 23 letra DNI % 23 letra DNI % 23 letra
0 T 8 P 16 Q
1 R 9 D 17 V
2 W 10 X 18 H
3 A 11 B 19 L
4 G 12 N 20 C
5 M 13 J 21 K
6 Y 14 Z 22 E
7 F 15 S

La función validar(dni,letra) determina si el par (dni, letra) definen un número de


identificación fiscal válido. Su diseño algorı́tmico se centra en la utilización de la tabla constante
TABLA NIF, que almacena las 23 letras válidas de los NIFs. Obsérvese que los elementos de esta
tabla no son datos numéricos, sino datos de tipo char.

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;
}

Función que determina la letra de un NIF

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];
}

9.3. Cálculos estadı́sticos con vectores de datos numéricos

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.

9.3.1. Cálculo de la media aritmética

La media aritmética x de una colección de datos {d1 , d2 , . . . , dn } viene determinada por la


siguiente relación.
n
X
di
i=1
x= n

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;
}

9.3.2. Cálculo de la desviación tı́pica

La desviación tı́pica o desviación estándar es una medida del grado de dispersión de


los datos de una colección con respecto al valor de su media aritmética. Dicho de otra manera,
la desviación tı́pica es simplemente el promedio o variación esperada con respecto a la media
aritmética. La desviación tı́pica σ de una colección de datos {d1 , d2 , . . . , dn } viene determinada
por la siguiente relación, en la que x representa la media aritmética de los n datos anteriores.
v
u n
uX
u
t (di − x)2
i=1
σ= n−1

La función desviacion(v,n) devuelve la desviación tı́pica de los primeros n elementos del v,


es decir, de v[0..n-1]. En su diseño se hace uso de las funciones pow(x,n) y sqrt(x), definidas
en la biblioteca predefinida cmath. La media aritmética de los n primeros elementos de un vector
v es calculada invocando la función media(v,n) que acaba de ser presentada en el subapartado
anterior. Apoyar el diseño de una función en otras diseñadas previamente es una práctica muy
recomendable en programació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.

La función moda(v,n), presentada a continuación, devuelve la moda de los primeros n


elementos del vector v, es decir, de v[0..n-1]. En el caso de que la distribución de datos
sea multimodal, la función moda(v,n) devuelve el valor de la moda almacenado en v con un
valor de ı́ndice menor. Ası́, en la colección { A, A, B, E, E, E, B, H, H, J, K, B, B, D, E, H, H}
el valor devuelto por la función moda(v,n) serı́a B y no el valor H, cuya frecuencia absoluta es
también de cuatro apariciones en la colección de datos.

/∗
∗ 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.

Los parámetros de una función que representen tablas multidimensionales serán


necesariamente parámetros por referencia. C++ exige que se expliciten las dimensiones de todos
sus ı́ndices, excepto el primero.

El programa que sigue realiza las siguientes operaciones:

1. Crea una matriz cuadrada de elementos de tipo int de dimensión DIM×DIM, donde DIM es
una constante global del programa.

2. Asigna valores pseudoaleatorios, comprendidos entre -10 y 10, a sus elementos.

3. Presenta por pantalla el contenido de la matriz, a razón de una fila por lı́nea de pantalla.

4. Transpone los elementos de la matriz.

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

using namespace std;

// Dimensión de las matrices con las que se va a trabajar


const int DIM = 12;

/∗
∗ 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

Caracteres y cadenas de caracteres

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.

10.1. Representación de caracteres

10.1.1. Tipos para la representación de caracteres

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

10.1.2. Relaciones entre caracteres y sus códigos numéricos

Determinar si un carácter corresponde a una letra mayúscula, a una minúscula o a un dı́gito


es muy sencillo. Basta hacer uso de los operadores de relación entre datos de tipo char.

/∗
∗ 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;
}
}

La función de conversión int(. . . ) permite extraer el código numérico de un dato de tipo


char y la función char(. . . ) permite deducir un carácter a partir de su código numérico.

// <codigo> toma el valor 59 (código ASCII de ;)


int codigo = int(’; ’ );
// <caracter> toma el valor ’;’
char caracter = char(codigo);

Al ejecutar el código mostrado a continuación se genera un listado de los códigos numéricos


de las letras minúsculas del alfabeto inglés.

for (char letra = ’a’ ; letra <= ’z’; ++letra) {


cout << setw(3) << int(letra) << setw(3) << letra << endl;
}

97 a
98 b
99 c
100 d
...
120 x
121 y
122 z

10.2. Representación de cadenas de caracteres

Una cadena o secuencia de caracteres es una sucesión de cero o más caracteres. Ejemplos:

Una cadena vacı́a de caracteres (sin ningún carácter): ""

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++.

Almacenando la secuencia de caracteres en una tabla o vector de datos de tipo char.

Creando un objeto de la clase predefinida string.

107
Capítulo 11

Estructuración agregada de datos

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

11.1.1. Concepto de registro

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.).

11.1.2. Selección de un campo de un registro

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.

11.2. Metodología de trabajo con registros

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.

11.2.1. Representación de un número de identificación fiscal

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.

Fichero de interfaz del módulo nif

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);

Fichero de implementación del módulo nif

La implementación de las funciones básicas especificadas anteriormente, en el fichero de interfaz, se


muestra a continuación. Las constantes NUM_LETRAS y TABLA_NIF son elementos auxiliares del módulo

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;

cout << "Escriba su DNI: ";


cin >> nifUsuario.dni;

cout << "Escriba su letra: ";


cin >> nifUsuario.letra;

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

Se presentan a continuación los ficheros de interfaz e implementación de un módulo fecha en el que


se define el tipo Fecha y dos funciones básicas para trabajar con los datos de ese tipo.

Fichero de interfaz del módulo 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

La implementación de las dos funciones básicas especificadas anteriormente, en el fichero de interfaz,


se muestra a continuación, junto con una función auxiliar, no visible fuera del módulo fecha, que codi-
fica numéricamente un dato de tipo Fecha para facilitar la implementación de la función esAnterior.
/*
* Fichero de implementación fecha.cpp del módulo <fecha>
*/
#include "fecha.hpp"
#include <iostream>
using namespace std;

/*
* 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);
}

11.3. Representación de una persona

A continuación se presentan los ficheros de interfaz e implementación del módulo persona en el


que se define el tipo Persona y un conjunto de funciones básicas para trabajar con los datos de ese tipo.

En el siguiente capítulo se trabajará en la resolución de diversos problemas planteados sobre vectores


cuyos elementos son datos del tipo Persona.

Fichero de interfaz del módulo persona

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

La función mostrar(p) que escribe en la pantalla los datos de una persona p

La función esMayorQue(persona1, persona2) que permite comprobar si persona1 es de mayor


edad que persona2

/*
* 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

A continuación se presenta el fichero de implementación del módulo.


/*
* Fichero de implementación persona.cpp 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

Algoritmos básicos de trabajo con


estructuras de datos indexadas

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.

12.1. Problemas de tratamiento de estructuras de datos indexadas

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:

Problemas de recorrido. Estos problemas requieren el tratamiento de todos los elementos de la


estructura. Por ejemplo, mostrar en la pantalla todos los elementos, copiarlos en otra estructura de
datos, determinar cuántos elementos satisfacen una determinada propiedad P, determinar cuál de
los elementos presenta un valor mínimo o máximo, etc. Para resolver estos problemas se requiere
tratar todos y cada uno de los elementos de la estructura mediante un algoritmo de recorrido.

Problemas de búsqueda. Estos problemas plantean la búsqueda de un elemento de la estructura


que satisfaga una determinada propiedad P. Cuando haya varios elementos que la satisfagan,
la localización de uno cualquiera de ellos bastará para resolver el problema. Los problemas de
búsqueda se resolverán mediante un algoritmo de búsqueda binaria o mediante un algoritmo
de búsqueda secuencial, en función de que los elementos de la estructura estén ordenados
respecto de la propiedad P o no lo estén.

Problemas de distribución de datos. Son problemas que requieren la reorganización de los


elementos de la estructura de forma que, en una parte de ella se sitúen los que satisfagan una
propiedad P y en la parte opuesta los que no la satisfagan. Para resolver estos problemas se
aplican algoritmos de distribución.

Problemas de ordenación de datos. Estos problemas requieren la reorganización de los ele-


mentos de la estructura de forma que queden ordenados según un determinado criterio. Para
resolver estos problemas se aplican algoritmos de ordenación.

En este capítulo se van a presentar algoritmos de recorrido, de búsqueda, de distribución y de or-


denación sobre estructuras de datos indexadas. Se presentará un esquema general de cada uno de los

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;
};

12.2. Algoritmos de recorrido

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.

12.2.1. Esquema algorítmico 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]]
}

12.2.2. Algunos algoritmos de recorrido

Mostrar los datos de un vector

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»
}
}

Contar los datos de un vector que satisfacen una propiedad

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;
}

Determinar el dato de un vector cuyo valor es mínimo o máximo

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];
}

12.3. Algoritmos de búsqueda

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.

Se presentarán esquemas generales de los algoritmos de búsqueda secuencial y binaria, seguidos de


ejemplos de aplicación de cada uno de ellos a problemas concretos de búsqueda. Se comienza por el de
búsqueda secuencial por la mayor simplicidad de su diseño.

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]

// Establece el espacio inicial de búsqueda T[i..n-1], es decir, T[0..n-1]


unsigned i = 0;
// Por el momento no se ha encontrado lo que se busca
bool encontrado = false;
while (!encontrado && i < n) {
// No ha habido éxito tras buscar en T[0..i-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 continúa en el espacio de búsqueda T[i+1..n-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;
}
}

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

/* Discriminación del éxito */


if (encontrado) {
return i;
}
else {
return -1;
}
}

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.

La estrategia de una búsqueda binaria en un vector ordenado, también denominada búsqueda


dicotómica, consiste en dividir en cada iteración en espacio de búsqueda en dos mitades y seleccionar
en cuál de ellas debe proseguirse con la búsqueda. La búsqueda concluye al reducirse el espacio de
búsqueda a un solo elemento, el número de iteraciones necesario para acotar la búsqueda a un solo
elemento es aproximadamente igual a log2 n, siendo n el número de elementos sobre el que se plantea
inicialmente la búsqueda.
/*
* Pre: n > 0 y los elementos de T[0..n-1] están ordenados de menor a mayor valor.
* Post: Si entre los datos almacenados en 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 buquedaBinaria (const Dato T[], const unsigned n) {
// Se ha programado un algoritmo de búsqueda binaria en vector ordenado según valores
// crecientes.

// 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

Se vuelve a diseñar una función similar a la función buscar(T, n, dniBuscado) presentada en la


sección anterior, partiendo de una premisa adicional: los elementos sobre los que se plantea la búsque-
da están ordenados según valores del DNI del NIF crecientes. Ello posibilita la aplicación de un
algoritmo de búsqueda binaria, tal como se presenta a continuación. El nombre dado a la nueva función
es buscarDicotomico(T, n, dniBuscado).
/*
* Pre: «T» tiene al menos «n» componentes y los elementos de las
* primeras «n» componentes del vector «T» ESTÁN ORDENADOS POR VALORES DEL
* DNI CRECIENTES.
* Post: Si entre las personas almacenadas en las primeras «n» componentes del
* vector «T» hay una cuyo DNI es igual a «dniBuscado», entonces ha
* devuelto el índice de dicho elemento en el vector; si no lo hay, ha
* devuelto un valor negativo.
*/
int buscarDicotomico(const Persona T[], const unsigned n,
const unsigned dniBuscado) {
if (n == 0) {
// Si hay 0 componentes, el dato no está
return false;
}
else {
// Espacio de búsqueda: establecimiento en T[0..n-1]
unsigned inf = 0;
unsigned sup = n - 1;

/* 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]

/* Discriminación del éxito */


if (T[inf].nif.dni == dniBuscado) {
return inf;
}
else {
return -1;
}
}
}

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.

12.4.1. Esquema algorítmico de distribución

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.

// Los elementos de T[inf..sup], es decir de T[0..n - 1], han de ser distribuidos.


int inf = 0;
int sup = n - 1;
while (inf < sup) {
// Todos los elementos de T[0..inf-1] satisfacen la propiedad P y ninguno de los
// elementos de T[sup + 1..n - 1] satisface P. Los elementos de T[inf..sup] han de ser
// distribuidos.
if (T[inf] satisface P) {
// T[inf] satisface P; por lo tanto este elemento está bien situado en <T>
inf = inf + 1;
}
else if (T[sup] satisface P) {
// T[sup] no satisface P; por lo tanto este elemento está bien situado en <T>
sup = sup - 1;
}
else {
// T[inf] no satisface P y T[sup] si satisface P; van a ser permutados
Dato aux = T[inf];
T[inf] = T[sup];
T[sup] = aux;
inf = inf + 1;
sup = sup - 1;
}
// Todos los elementos de T[0..inf-1] satisfacen P y ninguno de los de T[sup+1..n-1]
// satisface P. Los elementos de T[inf..sup] han de ser distribuidos
}
// Todos los elementos de T[0..inf - 1] satisfacen P y ninguno de los de T[inf..n - 1]
// satisface P. Por lo tanto, todos los elementos de T[0..n - 1] han sido distribuidos.
}

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.

12.5.1. Esquema algorítmico de ordenación por selección

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

// En cada iteración se permuta el elemento T[i] con el menor de los elementos


// de T[i..n-1]
for (unsigned i = 0; i < n - 1; ++i) {
// Los elementos de T[0..i-1] ya están ordenados según el criterio C
// Selecciona el elemento menor de T[i..n-1] según el criterio C
unsigned iMenor = i;
for (unsigned j = i + 1; j < n; ++j) {
// T[iMenor] es el menor de T[i..j-1] según el criterio C
if (T[j] es menor que T[iMenor] según el criterio C)
iMenor = j;
}
// T[iMenor] es el menor de T[i..j] según el criterio C
}
// T[iMenor] es el menor de T[i..n-1] según el criterio C.
// Permuta los elementos T[i] y T[iMenor]
Dato aux = T[i];
T[i] = T[iMayor];
T[iMayor] = aux;
// Los elementos de T[0..i] ya están ordenados según el criterio C
}
// Los elementos de T[0..n-1] ya están ordenados según el criterio C
}

12.5.2. Un algoritmo de ordenación por 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.

// En cada iteración se permuta el elemento T[i] con el menor de los elementos


// de T[i..n-1].
for (unsigned i = 0; i < n - 1; ++i) {
// Las personas de T[0..i-1] son las de más edad y ya están ordenadas
// según edades decrecientes.
// Selecciona la persona de más edad de T[i..n-1].
unsigned iMayor = i;
for (unsigned j = i + 1; j < n; ++j) {
// T[iMayor] es la persona de más edad de T[i..j-1].
if (esMayorQue(T[j],T[iMayor])) {
iMayor = j;
}
// T[iMayor] es la persona de más edad de T[i..j].
}
// T[iMayor] es la persona de más edad de T[i..n-1] y, por ello,
// permuta T[i] y T[iMayor].
permutar(T[i], T[iMayor]);
// Las personas de T[0..i] son las de más edad y ya están ordenadas.
}
// Las personas de T[0..n-1] ya están ordenadas.
}
}

13
Capítulo 13

Entrada y salida de datos

La escritura de un dato en pantalla o en un fichero es una operación de salida. La lectura de un dato


suministrado a través del teclado o almacenado en un fichero es una operación de entrada.

Este capítulo constituye una introducción a la programación de operaciones de entrada y salida de


datos desde un programa. Solo se va a presentar un conjunto mínimo de herramientas para facilitar la
programación de estas operaciones. En capítulos posteriores, en las prácticas de esta u otras asignaturas
o de forma autónoma se profundizará en las posibilidades que el lenguaje ofrece para programar otras
operaciones más sofisticadas de entrada y salida de datos.

13.1. Entrada y salida de datos

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:

Al teclado desde el que el operador puede facilitar datos al programa.

A la pantalla a través de la cual el programa puede comunicar al operador ciertos resultados.

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.

La comunicación de datos entre un programa C++ y el exterior está fundamentada en el concepto de


flujo o stream. Un flujo permite comunicar información desde un origen hacia un destino. El propio
programa C++ es uno de los extremos de dicho flujo. El otro extremo puede ser un dispositivo físico
(teclado o pantalla) o un fichero o archivo almacenado en un dispositivo físico (cualquier dispositivo de
almacenamiento).

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:

La biblioteca predefinida <iostream>. Ofrece al programador cuatro flujos o streams predefinidos,


cin, cout, cerr y clog, para facilitar la interacción de los programas C++ con el teclado y la
pantalla del terminal del operador.

La biblioteca predefinida <istream>. Permite al programador definir flujos de dos tipos:

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.

La biblioteca predefinida <ostream>. Permite al programador definir un nuevo tipo de flujos:

• 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.

13.2. Operaciones de entrada y salida para la interacción con el ope-


rador

13.2.1. Flujos estándar predefinidos para la interacción con el operador

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

Las operaciones definidas en la clase istream facilitan la programación de la lectura de secuencias


de datos desde un flujo de entrada. Para conocer estas operaciones conviene consultar un manual del
lenguaje. Entre ellas destacaremos la operación de lectura que realiza la conversión automática del
formato de los datos leídos:

>>: operador para la extracción de datos con formato de un flujo de entrada.


Está disponible para datos de tipo primitivo (int, unsigned, double, char, bool, ...) y para datos
de tipo string.
Es el operador que el que hemos estado trabajando con cin hasta el momento.

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):

f.get(char& c): extrae un carácter del flujo de entrada f y lo asigna al parámetro c.

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>.

13.2.3. Operaciones definidas en la clase ostream para la escritura de datos

Las operaciones definidas en la clase ostream facilitan la programación de la escritura de secuencias


de datos en un flujo de salida. Entre ellas destacaremos la operación que permite escribir datos con
formato en un flujo:

<<: operador para la inserción de datos con formato en un flujo de salida.


Está disponible para datos de tipo primitivo (int, unsigned, double, char, bool, ...) y para datos
de tipo string.
Es el operador que el que hemos estado trabajando con cout hasta el momento.

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):

f.put(const char c): inserta el carácter c en el flujo de salida f.

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.

13.3. Entrada y salida de datos en ficheros

Los computadores almacenan ficheros o archivos en sus dispositivos de almacenamiento secun-


dario (discos internos y externos, memorias USB, dispositivos ópticos, sistemas de ficheros en la nube,
etc.) que pueden contener información de naturaleza muy diversa: textos, datos, música, imágenes,
vídeo, etc.

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;
}
}

Para leer la información almacenada en un fichero, lo asociaremos a un flujo de entrada de la clase


ifstream.

La función mostrar(nombreFichero) abre el fichero en modo lectura al ejecutar la instrucción


f.open(nombreFichero). Si esta operación se realiza con éxito entonces lee, uno a uno, todos los carac-
teres del fichero mediante la invocación f.get(c), que los va asignando, uno a uno en cada iteración,
a la variable c, para después, en el cuerpo del bucle, escribir los caracteres leídos en la pantalla.

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:

1. Sintácticamente, la invocación f.get(c) que estamos utilizando, devuelve una referencia a un


objeto de la clase istream que es el propio flujo f.
Análogamente, la utilización del operador de inserción o de extracción con un flujo, también
devuelve una referencia al flujo que se ha estado utilizando. Esto es lo que sintácticamente nos
ha permitido estar escribiendo lecturas del teclado como cin >>a >>b >>c; y escrituras en la
pantalla como cout <<"La suma es "<<a + b <<endl;

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 .

f.open(nombreFichero): asocia al flujo f un fichero de nombre nombreFichero para proceder a


la lectura de sus datos.

f.is_open(): devuelve true si y solo si el flujo f está asociado a un fichero.

f.close(): libera el fichero asociado al flujo f y lo disocia de este.

f.get(c): lee el siguiente carácter del flujo f y se lo asigna a c.

operador de conversión a bool: la invocación f.get(c), cuando se utiliza como condición de


iteración del bucle while, se evalúa como true si la última operación de lectura se llevó a cabo
con éxito y por tanto se ha extraído el siguiente carácter del flujo f, se ha asignado a c y está
pendiente de ser procesado. Se evalúa como false en caso contrario, es decir, cuando la última
operación de lectura no se pudo llevar a cabo correctamente, por no haber ya datos pendientes
de lectura en el flujo de entrada f.

13.3.2. Escritura de datos de un fichero

Para escribir o almacenar información en un fichero, lo asociaremos a un flujo de salida de la clase


ofstream.

La función copiar(nombreFichero, nombreCopia) permite crear un fichero con el nombre


nombreCopia en el cual se escribe una copia literal de los datos almacenados en el primer fichero cuyo
nombre es nombreFichero. Para ello se abre el fichero en modo escritura, instrucción
original.open(nombreFichero), creándolo si no existía previamente o borrando su contenido, si ya
existía.

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 .

f.open(nombreFichero): asocia al flujo f un fichero de nombre nombreFichero para proceder a


la escritura de datos en él; si no existía un fichero con ese nombre, será creado y, si ya existía,
será borrado su contenido previo.

f.close(): libera el fichero asociado al flujo f y lo disocia de este.

f.put(c): añade el carácter c al final del contenido del flujo f.

2
Por ejemplo: http://www.cplusplus.com/reference/fstream/ofstream/

7
Capítulo 14

Trabajo con ficheros de texto

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.

14.1. Ficheros de texto

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.

14.1.1. Un texto literario

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.

Yo pensé que no hallara consonante,


y estoy a la mitad de otro cuarteto;
mas si me veo en el primer terceto,
no hay cosa en los cuartetos que me espante.

Por el primer terceto voy entrando,


y parece que entré con pie derecho,
pues fin con este verso le voy dando.

Ya estoy en el segundo, y aun sospecho


que voy los trece versos acabando;
contad si son catorce, y está hecho.

14.1.2. Una secuencia de números de identificación fiscal

Un fichero de texto puede almacenar una colección de datos formateados todos ellos como secuencias
de caracteres.

El fichero de texto que se muestra a continuación almacena la información correspondiente a los


números de identificación fiscal (NIF) ficticios de 30 personas.

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.

En este caso la descripción de la sintaxis se limita al siguiente conjunto de reglas:

<fichero-nif> ::= { <nif> }


<nif> ::= <dni> <separador> <letra> <fin-línea>
<dni> ::= literal-entero
<letra> ::= "A" | "B" | "C" | "D" | "E" | "F" | "G" | "H" | "J" | "K" | "L" | "M"
| "N" | "P" | "Q" | "R" | "S" | "T" | "V" | "W" | "X" | "Y" | "Z"
<separador> ::= "-"
<fin-línea> ::= "\n"

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.

14.2.1. Recuento del número de líneas y de caracteres de un texto

La función contabilizar(nombreFichero, nLineas, nCaracteres) analiza el contenido del texto


almacenado en un fichero denominado «nombreFichero» y asigna a sus parámetros segundo y tercero,
nLineas y nCaracteres, el número de líneas del texto y el número total de caracteres de todas sus líneas,
incluyendo en la cuenta el carácter de fin de cada línea.
/*
* Pre: «nombreFichero» es el nombre de un fichero de texto existente y accesible para su
* lectura.
* Post: Si el fichero de nombre «nombreFichero» se puede leer, asigna a «nLineas» el número de
* líneas que contiene del fichero y a «nCaracteres», el número de caracteres del mismo y
* devuelve «true». En caso contrario, devuelve «false».
* Nota: Solución que lee línea a línea.
*/
bool contabilizar(const string nombreFichero, unsigned& nLineas,
unsigned& nCaracteres) {
ifstream f; // Declara un flujo de entrada.
f.open (nombreFichero); // Asocia a «f» el fichero «nombreFichero».
if (f.is_open()) {
nLineas = 0;
nCaracteres = 0; // Solución inicial provisional
string linea; // Para almacenar las líneas leídas
while (getline(f, linea)) {
// Mientras el último intento de lectura fue correcto
nLineas++; // Actualiza el número de líneas...
nCaracteres += linea.length() + 1; // ... y caracteres.
}
f.close(); // Libera el fichero asociado a «f».
return true;
}
else {
cerr << "No se ha podido abrir el fichero \"" << nombreFichero << "\"."
<< endl;
return false;
}
}

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

La función analizar(nombreFichero, frecuencias) analiza el contenido del texto almacenado en


un fichero de nombre «nombreFichero» y asigna al vector frecuencias el número de apariciones en el
fichero de cada una de las letras del alfabeto inglés.
/*
* Pre: «nombreFichero» es el nombre de un fichero de texto válido y el número de componentes
* del vector «frecuencias» es igual al número de letras del alfabeto inglés.
* Post: Asigna a cada componente del vector «frecuencias» el número de apariciones de cada una
* de las letras del alfabeto inglés en el fichero cuyo nombre es «nombreFichero»,
* no distinguiendo entre mayúsculas y minúsculas. La primera componente del vector es el
* número de veces que aparece la letra A, la segunda, las de la letra B y así
* sucesivamente.
*/
void analizar(const string nombreFichero, unsigned frecuencias[]) {
// Inicializa el vector de frecuencias
for (char c = 'A'; c <= 'Z'; c++) {
frecuencias[c - 'A'] = 0;
}

ifstream f; // Declara un flujo de entrada


f.open(nombreFichero); // Lo asocia al fichero nombreFichero
if (f.is_open()) {
char c;
while (f.get(c)) {
// Mientras el último intento de lectura fue correcto
// Contabiliza el carácter si y solo si es alfabético
if (isalpha(c)) {
frecuencias[toupper(c) - 'A']++;
}
}
f.close(); // Libera el fichero y lo disocia de f
}
else {
cerr << "No se ha podido leer del fichero \"" << nombreFichero << "\"." << endl;
}
}

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.

14.2.3. Reconocimiento de los datos almacenados en un fichero de texto

La función leerFicheroNif(nombreFichero, T, nDatos) ilustra cómo leer un fichero de texto y


extraer la información relevante almacenada en él.

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

La función escribirFicheroNif(nombreFichero, T, n) ilustra cómo crear un fichero de texto. Al


ser invocada creará un fichero de texto que almacena los NIF definidos en los n primeros elementos
del vector T. Los elementos de este vector son datos del tipo Nif, definidos en el tema 11. Los datos se
almacenan en el fichero de forma textual, según la primera de las sintaxis descritas anteriormente.

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;
}
}

También podría gustarte