ProgramacionLibro Alumnos
ProgramacionLibro Alumnos
JAVIER S ANGUINO
C
CC BY: $
\
Esta obra está sujeta a la licencia Reconocimiento-NoComercial-CompartirIgual 4.0 Internacional de Creative Commons. Para ver una copia
de esta licencia, visite http://creativecommons.org/licenses/by-nc-sa/4.0/.
Índice general
1. Elementos de un ordenador 1
1.1. Introducción . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
1.1.1. Breve apunte histórico . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
1.2. Componentes de un ordenador . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
1.3. Disposición de la memoria . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
1.3.1. Introducción . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
1.3.2. Nomenclatura . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
1.3.3. Disposición . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
1.4. Sistemas Operativos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
1.5. Compilación . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
1.6. Programación . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
3. Introducción al lenguaje C 27
3.1. Introducción . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
3.2. Datos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
3.2.1. Tipos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
3.2.2. Identificadores de datos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40
3.2.3. Dirección de los datos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40
3.3. Funciones básicas de entrada–salida . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41
3.3.1. printf() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41
3.3.2. scanf() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45
3.3.3. getchar() y putchar() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50
3.4. Operadores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51
3.4.1. Operador de asignación . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51
3.4.2. Operadores aritméticos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52
3.4.3. Operadores de asignación compuestos . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55
3.4.4. Operadores incrementales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55
3.4.5. Operadores de relación . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56
3.4.6. Operadores lógicos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57
I
II Índice general
6. Punteros 143
6.1. Introducción . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 143
6.2. Variables y punteros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 143
6.2.1. Aritmética básica de punteros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 146
6.3. Vectores y punteros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 147
Índice general III
7. Funciones 171
7.1. Introducción . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 171
7.2. Aspectos fundamentales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 172
7.2.1. Tipos de las funciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 172
7.2.2. Sentencia return . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 173
7.2.3. Declaración de funciones y prototipos . . . . . . . . . . . . . . . . . . . . . . . . . . . . 173
7.2.4. LLamadas a funciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 173
7.2.5. Primeros ejemplos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 173
7.3. Visibilidad de las variables . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 185
7.4. Regiones de la memoria . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 188
7.4.1. Gestión dinámica y direccionamiento dinámico . . . . . . . . . . . . . . . . . . . . . . . 190
7.5. Paso de argumentos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 191
7.6. Funciones y vectores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 194
7.6.1. Ejemplos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 194
7.6.2. Funciones, cadenas de caracteres y estructuras . . . . . . . . . . . . . . . . . . . . . . . 197
7.7. Funciones y matrices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 197
7.8. Punteros a funciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 199
7.9. Recursividad . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 203
7.9.1. Ejemplo: Torres de Hanoi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 205
7.10. Problemas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 208
Bibliografía 243
CAPÍTULO 1
Elementos de un ordenador
1.1. Introducción
Ya se ha comentado en la Presentación de este documento, que aunque el lenguaje C dispone de las estructuras
típicas de los lenguajes de alto nivel, a su vez dispone de comandos u órdenes que permiten un control a bajo nivel.
Esta característica hace que antes de comenzar con el aprendizaje del lenguaje C, sea importante plantear una serie
de conceptos relacionados con el funcionamiento de un ordenador, que faciliten este estudio y permitan utilizar
correctamente algunas de las herramientas que ofrece este lenguaje, para realizar códigos eficientes y flexibles.
John von Neumann. Nació en Budapest en 1903. Es uno de los matemáticos más brillantes de la historia
(recomiendo consultar Wikipedia). Hizo numerosas y significativas contribuciones en diferentes áreas de las
matemáticas y física, desde la rama pura a la más aplicada. Trabajó en el Instituto de Estudios Avanzados de
Princeton, junto con otros científicos de la época (entre ellos Albert Einstein). Fue así mismo, un científico
destacado en el Proyecto Manhatann dirigido por J.R. Openheimmer. En aquella época era habitual sub-
estimar los peligros de la radiación y como consecuencia de las sufridas durante los ensayos de la Bomba
Atómica, murió en 1957 en Washington D.C.
Destacó en las Ciencias de la computación contribuyendo en cómo debía ser la configuración física de
un ordenador y dando lugar a la llamada arquitectura von Neumann, que es utilizada en casi todos los
computadores actuales. Su concepto se basa en tres pilares:
1. Una unidad de control que se encargaría de organizar el trabajo de la máquina: regular las instruccio-
nes, su cálculo e intercambio de información con el exterior.
2. Una memoria importante que fuera capaz de almacenar no sólo datos sino también instrucciones.
3. Un programa que indicase a la máquina los pasos a seguir sin necesidad de esperar las sucesivas
órdenes desde el exterior, como se venía haciendo desde entonces.
En 1949 construyó un ordenador con estas características: EDVAC. El primer ordenador comercial fabricado
con esta configuración fue el UNIVAC I en 1951.
Alan Turing. Nació en Londres en 1912. Fue matemático y es considerado uno de los padres de la ciencia
de la computación siendo el precursor de la informática moderna. Proporcionó una influyente formalización
1
2 Capítulo – 1. Elementos de un ordenador
de los conceptos de algoritmo y computación: la máquina de Turing. En cierta manera estableció los pilares
teóricos sobre los conceptos de algoritmo y programa, considerando que la resolución de ciertos problemas
podían ser descompuestos en tareas más simples abordables de manera automática.
Tuvo una aportación clave en la II S EGUNDA G UERRA M UNDIAL, al contribuir de manera significativa en
el descubrimiento de la codificación de la máquina Enigma y permitir a los aliados anticipar los ataques y
movimientos militares nazis.
En 1952 Turing reconoció en un juicio su homosexualidad, con lo que se le imputaron los cargos de in-
decencia grave y perversión sexual (los actos de homosexualidad eran ilegales en el Reino Unido en esa
época). Convencido de que no tenía de qué disculparse, no se defendió de los cargos y fue condenado. Dos
años después del juicio, en 1954, Turing se suicidó mediante la ingestión de una manzana contaminada con
cianuro.
Unidades de salida. Son los instrumentos por el que se muestran los resultados de los programas ejecutados en
el ordenador. Estos dispositivos transforman las señales binarias en información inteligible para el usuario:
pantalla, altavoz, impresora, etc.
Unidad central de proceso (CPU, Central Processing Unit). Se trata de un microprocesador que constituye el
cerebro del ordenador (véase figura 1.2). Permite sincronizar las diferentes instrucciones que deben eje-
cutarse en cada instante y además proporciona señales de temporización y de control que posibilitan la
introducción y eliminación de las instrucciones y datos, según se vayan realizando. Por tanto, se trata de un
dispositivo que procesa instrucciones básicas que son necesarias tanto para el correcto funcionamiento del
ordenador, como para la ejecución adecuada de los programas.
Alguno de los elementos básicos que constituyen la CPU son
Unidad de control ( CU, Control Unit). Detecta las las señales eléctricas de estado procedentes de las
distintas unidades, indicando su situación o condición de funcionamiento (véase figura 1.2). También
J. Sanguino
1.2 – Componentes de un ordenador 3
c
Memoria Secundaria o Externa (ME)
d, i
Unidades centrales
Procesador (CPU)
d,i d
d
e e e c
recibe secuencialmente de la memoria las instrucciones del programa en ejecución, y de acuerdo con el
código de operación de la instrucción captada y con las señales de estado procedentes de los distintos
dispositivos del ordenador, genera señales de control dirigidas a todas las unidades, ordenando las
operaciones que implican la ejecución de una instrucción.
La Unidad de control contiene un reloj, que es un generador de impulsos eléctricos que sincroniza
todas las operaciones elementales del ordenador. El período de esta señal se denomina tiempo de ciclo
y está comprendido aproximadamente entre décimas de nanosegundos y microsegundos, dependiendo
del procesador. La frecuencia del reloj (inverso del tiempo de ciclo) suele expresarse en millones
de ciclos/segundo (Megahercios o MHz) o miles de millones de ciclos/segundo (Gigahercios o GHz).
La ejecución de cada instrucción supone la realización de un conjunto de operaciones elementales
consumiendo un número predeterminado de ciclos, de forma que las instrucciones más complejas
utilizan un número mayor de ciclos que las más simples (véase Cantone [3, pág. 46]).
Unidad aritmético-lógica ( ALU, Arithmetic Logic–Unit). Contiene los circuitos electrónicos con los
que se hacen las operaciones del tipo aritmético (sumas, restas, etc) y de tipo lógico (comparar dos
números, operaciones del álgebra de Boole binaria, etc.). Está directamente relacionada con ciertos
registros que están en el interior de la CPU y que almacenan temporalmente datos e instrucciones, con
el objetivo de operar con ellos.
Registros. Se llama registro a una pequeña memoria diseñada para almacenar un dato, instrucción
o dirección de memoria. Están constituidos por un conjunto de bits (ya se definirá más adelante),
normalmente 32 ó 64 bits. Existen diferentes tipos de registro dentro de la CPU
• Registro de Contador de Programa. Se trata de un registro que almacena la dirección de memoria
de la siguiente instrucción a ejecutar.
• Registro de Instrucciones. Se almacena la instrucción que en ese momento ejecuta la Unidad de
Control (CU).
• Registro de dato de memoria. Almacena la información (instrucción o dato de instrucción) y cuya
dirección se encuentra en el Registro de dirección de memoria.
• Registro de dirección de memoria. Almacena la dirección de la instrucción o dato que se encuentra
en el Registro de dato de memoria.
• Matriz de Registros. Consiste en una disposición de registros de propósito general, que se emplean
en el proceso intermedio de ejecución de instrucciones. Una de sus utilidades consiste en almace-
J. Sanguino
4 Capítulo – 1. Elementos de un ordenador
nar los datos y direcciones de memoria, que serán necesarios en los procesos siguientes. La ALU
puede acceder muy rápidamente a estos registros, lo que permite que el programa se ejecute de
manera más eficiente.
Memoria interna (MI). Es la unidad que almacena tanto los datos, como las instrucciones durante la ejecución
de los programas. La memoria interna (también se denomina memoria central o memoria principal) actúa
con una gran velocidad y está ligada directamente a las unidades más rápidas del computador: unidad de
control y unidad aritmético–lógica. Para que un programa se ejecute debe estar almacenado (o cargado) o
al menos parte, en la memoria principal.
Normalmente hay una zona de la memoria que sólo se puede leer, llamada ROM (Read Only Memory) y
que es permanente (se mantiene aunque se desconecte el ordenador) y otra en la que se puede leer y escribir,
llamada RAM (Random Access Memory) pero que es volátil.
La memoria ROM suele venir grabada de fábrica y contiene un conjunto de programas llamados BIOS
(Basic Input/Output System) encargados de hacer posible inicialmente, el acceso y la gestión básica de
algunos dispositivos, como el disco duro y los puertos (conexiones externas de entrada/salida). Contiene
subrutinas de inicialización de parámetros y gestiona determinadas solicitudes procedentes de los periféricos,
denominadas interrupciones. Así mismo, posee las instrucciones para buscar y cargar el Sistema Operativo
del dispositivo de almacenamiento secundario, en el momento de encender el ordenador.
Por otra parte, la memoria RAM constituye el espacio de trabajo de la CPU. En ella se almacenan los pro-
gramas y también reside el Sistema Operativo cuando está activado el ordenador. Se divide a su vez, en dos
tipos
RAM Dinámica (DRAM). En los ordenadores actuales esta memoria está formada por capacitores,
dispuestos en circuitos electrónicos integrados, llamados chips (véase Cantone [3, pág.50]). Debido a
que tiende a descargarse es necesario refrescarlas periódicamente.
RAM Estática (SRAM). Los datos se almacenan de manera fija, por lo que no necesitan refresco. Son
más rápidas que las memorias DRAM y más caras. Estás memorias también se conocen como memorias
Caché. Su objetivo es contener información que utiliza frecuentemente el procesador, de esta manera
no es necesario descargarlas de la DRAM constantemente y aumentan la rapidez de ejecución. Existen
diferentes niveles:
• L1 . Es una memoria que va desde los 32 KB a los 64 KB1 . Tiene la particularidad de estar dentro
del procesador, por eso este tamaño reducido, pero por contra es muy rápida.
• L2 . Es de mayor tamaño que la anterior (puede llegar a un 512 KB). No está situada dentro del
microprocesador pero también puede acceder a ella muy rápidamente.
• L3 . Análoga a la L2 , pero de tamaño mayor a ésta (puede llegar a 2 ó 3 MB.)
La aparición de procesadores (CPU ’ S) de más de un núcleo2 ha replanteado la estructura de la memoria
Caché. En este sentido, la compañía Intel ha desarrollado una memoria llamada Smart Caché (que
anuncia como característica Caché en sus microprocesadores) y que grosso modo, consiste en que
cada núcleo posea su Caché del nivel L1 , pero junta los niveles L2 y L3 , que son compartidos por los
núcleos del microprocesador (véase figura 1.3). De esta manera, la memoria Smart Caché puede llegar
a tener un tamaño de 3 Mb.
Memoria externa (ME). La zona RAM de la memoria interna es muy rápida, pero no tiene gran capacidad y es
volátil (esto quiere decir que cuando se apaga el ordenador, se pierde la información almacenada en ella).
Para guardar masivamente información de manera permanente, se utilizan otros tipos de memoria, que están
dispuestos en discos magnéticos, discos ópticos y cintas magnéticas. Actualmente se utilizan también discos
duros externos con conexiones USB. El acceso a los datos es mucho más lento (alrededor de un millón de
veces), pero es un modelo permanente y la capacidad de almacenamiento es mucho mayor (alrededor de
mil veces). El conjunto de estas unidades se llama memoria externa, secundaria o auxiliar. Usualmente
1
Más adelante, en el cuadro 1.1 se presentan las medidas de la memoria de un ordenador.
2
Se entiende por núcleo (o core) al conjunto formado por una ALU y una CU
J. Sanguino
1.2 – Componentes de un ordenador 5
Núcleo Núcleo
(CPU) (CPU)
Buses
los datos y programas se graban en la memoria externa, de esta forma cuando se ejecuten varias veces,
no es necesario introducirlo por los dispositivos de entrada. La información almacenada permanece en el
dispositivo indefinidamente, hasta que se borre expresamente por el usuario.
Buses. La transmisión de la información a través de los distintos dispositivos del ordenador se realiza a través
de un conjunto de hilos, líneas o pistas de conductores eléctricos, llamados buses. Sirven para interconectar
dos o más componentes funcionales de un sistema o de varios sistemas distintos. Estas conexiones suelen
llevar en un instante dado la información completa de una instrucción, un dato o una dirección. El ancho
de bus es el número de hilos que contiene, o número de bits que transmite simultáneamente en paralelo
(véase Floyd [5, pág. 786] o bien Cantone [3, pág. 49]). Por tanto, un bus se caracteriza por la cantidad
de información que se transmite a través de estos hilos, de forma simultánea. Este volumen se expresa
en bits y corresponde al número de líneas físicas (hilos) mediante las cuales se envía la información en
forma simultánea. Un cable plano de 32 hilos permite la transmisión de 32 bits en paralelo. Así pues, el
propósito de los buses es poder transmitir información de manera simultánea y reducir el tiempo de acceso
a la información entre los distintos componentes, al realizar las comunicaciones a través de un sólo canal
de datos. Ésta es la razón por la que, a veces, se utiliza la metáfora autopista de datos. En el caso en que
sólo dos componentes de hardware se comuniquen a través de la línea, podemos hablar de puerto hardware
(puerto serial o paralelo).
Por otra parte, la velocidad del bus se define a través de la frecuencia (medida en Hercios), es decir el
número de paquetes de datos que pueden ser enviados o recibidos por segundo. Cada vez que se envían o
reciben estos datos podemos hablar de ciclo. Este proceso está directamente relacionado con la velocidad de
la Unidad de Control.
Así pues, es posible hallar la velocidad de transferencia máxima del bus (la cantidad de datos que puede
transportar por unidad de tiempo) al multiplicar su ancho por la frecuencia. Por lo tanto, un bus con un ancho
de 16 bits y una frecuencia de 133 MHz, tiene una velocidad de transferencia de:
En realidad, cada bus se halla generalmente constituido por hilos eléctricos cuyo número suelen ser potencias
de 2 (25 = 32, 26 = 64, 27 = 128, etc). Estos hilos se dividen a su vez en tres subconjuntos:
El bus de direcciones (o bus de memoria) transporta las direcciones de memoria al que el procesador
desea acceder, para leer o escribir datos. Se trata de un bus unidireccional.
El bus de datos transfiere tanto las instrucciones que provienen del procesador como las que se dirigen
hacia él. Se trata de un bus bidireccional.
El bus de control transporta las órdenes y las señales de sincronización que provienen de la unidad
de control y viajan hacia los distintos componentes de hardware. Se trata de un bus bidireccional en la
medida en que también transmite señales de respuesta del hardware.
J. Sanguino
6 Capítulo – 1. Elementos de un ordenador
En algunas ocasiones se habla de buses multiplexados que utilizan líneas eléctricas multiplexadas para el
bus de direcciones y el bus de datos, por ejemplo. Esto significa que un mismo conjunto de líneas eléctricas
se comportan unas veces como bus de direcciones y otras veces como bus de datos, pero nunca al mismo
tiempo. Una línea de control permite discernir cual de las dos funciones está activa.
1.3.2. Nomenclatura
Desde un punto de vista electrónico no es difícil confeccionar un dispositivo que tenga únicamente dos estados:
encendido o apagado (véase Cantone [3, pág. 44]). A este tipo de dispositivo se le denomina BIT (BInary digiT).
Es decir, se trata de una posición, celda o variable que toma el valor 0 o 1. Resulta obvio que la información que
se puede almacenar en un bit, resulta muy pobre. Para aumentar esta capacidad de información se define el byte,
que es un conjunto de 8 bits. Esto aumenta claramente la información, pues se pasa de 2, a 28 = 256 maneras
diferentes de representación. Así pues, un byte es una disposición de 8 bits, que puede tomar 256 valores (todos en
forma de 0’s y 1’s). Es habitual considerar el byte como medida de bits. Así pues, se habla 3 bytes en lugar de 24
bits.
Puesto que los bytes (en particular los bits) se pueden utilizar para almacenar información, el tamaño de la
memoria de un ordenador se mide mediante la cantidad de bytes disponibles o a los que puede acceder la CPU. Con
el paso de los años la tecnología se va perfeccionado y la capacidad de los ordenadores va en aumento. Es habitual
utilizar la nomenclatura de la tabla 1.1, para medir el tamaño de la memoria. Como puede observarse, se utilizan
factores de 210 = 1024, en lugar de 103 para definir las diferentes unidades.
1.3.3. Disposición
Para que el acceso a la memoria por parte de la CPU sea lo más eficiente posible, la memoria se organiza en
grupos de celdas (cada celda correspondería a un bit) y a cada grupo de celdas se le asigna un número (entero sin
J. Sanguino
1.4 – Sistemas Operativos 7
signo) que se llama dirección. Por lo general (principalmente en los ordenadores domésticos), a cada 8 celdas es
decir a cada byte, se le asigna una dirección, que suele ser el tamaño asociado a una variable de tipo carácter (por
tanto en un byte se pueden almacenar 256 tipos de caracteres).
La siguiente cuestión es cuántos bytes son direccionables. Este hecho depende del procesador y del sistema
operativo. En lo que se refiere al procesador, los primeros (por ejemplo el Intel 8086 en 1979) utilizaban dos bytes
para el bus de direcciones, con lo cual sólo podía direccionar 216 bits = 64 KB de memoria. En la actualidad
se tienen procesadores con 32 bits con lo cual pueden direccionar 232 bits = 4 GB y los últimos ya utilizan
direccionamiento de 64 bits (264 bits = 16 EB). Una representación de la disposición de la memoria interna en un
ordenador puede verse en la figura 1.4.
Direcciones en
Celdas de memoria hexadecimal
(un byte)
0 0 0 0
0 0 0 1
0 0 0 2
0 0 0 3
64 KB
D F F F
E F F F
F F F F
J. Sanguino
8 Capítulo – 1. Elementos de un ordenador
Organiza los datos de los dispositivos de almacenamiento secundario (sistema de ficheros) de una manera
particular. No lo hacen de la misma forma Linux, Windows o IOS.
Determina el orden en el que las tareas deben ejecutarse en la CPU y el tiempo que ésta dedica a la ejecución
de cada una de ellas. En este caso, un sistema operativo puede ofrecer multiproceso o paralelización si la
CPU dispone de varios núcleos.
Gestiona la conexión de una posible red de ordenadores, incluso permite que el ordenador se pueda trans-
formar en un servidor de otros posibles ordenadores conectados.
En la actualidad es posible encontrar SO de 32 bits (cada vez menos) o de 64 bits. Si un SO es de 32 bits, quiere
decir que únicamente puede direccionar memoria hasta 232 bits = 4 Gb. Los SO suelen ser programas muy flexibles
y aunque un ordenador tenga un procesador que permita direccionar hasta 64 bits de memoria, es posible instalar
un SO de 32 bits y que el ordenador funcione con normalidad. Sin embargo, resulta mucho más eficiente instalar un
SO cuyo direccionamiento de memoria se ajuste al direccionamiento de memoria del procesador. Análogamente
sucede con las aplicaciones (procesadores de texto, hojas de cálculo, compiladores, programas de cálculo,. . . ), no
es lo mismo ejecutar un programa que es de 64 bits (esto es, es capaz de gestionar direcciones de memoria de 64
bits) sobre un SO de 32 bits, que en uno de 64 bits. Siempre hay que tratar que el tamaño de direccionamiento de
memoria de los procesadores, SO y programas coincidan para obtener un mayor rendimiento.
Hay diferentes SO en el mercado. Los más populares son Windows, Linux e IOS, cada uno realiza la gestión
entre usuario y computadora de manera distinta por lo general, no compatible entre ellos. En la actualidad todos
disponen de versiones de 64 bits, con lo cual sobre procesadores capaces de gestionar direcciones de memoria de
64 bits, hacen que la computadora permita gestionar y ejecutar de manera eficiente un gran número de potentes
programas (siempre que sean a su vez, de 64 bits).
LLegado a este punto se puede describir de manera aproximada, el proceso de arranque de una computadora:
1. La CPU se dirige a la ROM y carga la BIOS. Seguidamente realiza un autotest para comprobar todos los
componentes principales y la memoria.
2. La BIOS proporciona información acerca del almacenamiento secundario, la secuencia de arranque y carga
en la RAM el SO.
1.5. Compilación
Cuando un usuario desea resolver un problema de naturaleza repetitiva mediante un computador es necesario
traducir dicho problema a un lenguaje de programación, a través de un programa (generalmente identificado
como código fuente). Posteriormente, para que un ordenador ejecute dicho programa es necesario que este código
esté traducido a un lenguaje que el ordenador sepa entender (llamado código máquina). Los lenguajes de progra-
mación o traductores se encargan de esta tarea y pueden dividirse en dos grupos: intérpretes y compiladores.
Intérpretes. Se trata de un traductor que toma el programa fuente, almacenado en un fichero fuente y lo traduce y
lo ejecuta, línea por línea. Los primeros lenguajes (por ejemplo el BASIC) eran interpretados. Generalmente
son lentos, debido a que deben volver a traducir cualquier código que se repita, pues una vez traducida al
código máquina y ejecutada una línea, se olvida. Actualmente la mayoría de los intérpretes modernos, como
los del lenguaje PYTHON, traducen el programa entero en un lenguaje intermedio, que es ejecutable por un
intérprete mucho más rápido. Tal y como comenta Eckel [1, Cap. 2], los límites entre los compiladores y
J. Sanguino
1.6 – Programación 9
los intérpretes tienden a ser difusos, especialmente con PYTHON, que tiene muchas de las características y
el poder de un lenguaje compilado, pero también tiene parte de las ventajas de los lenguajes interpretados.
Otro ejemplo actual de un lenguaje interpretado es MATLAB.
Compiladores. Un compilador consiste en un programa que traduce los programas almacenados en ficheros
fuente a un lenguaje máquina. El resultado final suele ser un fichero que contiene el código máquina y
recibe el nombre de fichero ejecutable, que se identifica por la extensión .exe . El lenguaje C utiliza la
compilación para traducir los programas a lenguaje máquina.
A diferencia de los intérpretes, los programas generados por un compilador habitualmente requieren mucho
menos espacio para ser ejecutados y se realizan mucho más rápido. Adicionalmente, el C está diseñado
para admitir trozos de programas compilados independientemente. Estas partes se combinan mediante una
herramienta llamada enlazador (linker), para generar el fichero ejecutable. La forma de realizar la traducción
de un código fuente mediante un compilador suele constar de varias etapas:
Hay que mencionar que existen muchos compiladores de C en el mercado. En este curso se utilizará el compilador
TDM - GCC 4.9.2 que emplea el entorno de compilación DEV- C ++ 5.11, que es de libre distribución y que puede
descargarse de la dirección http://orwelldevcpp.blogspot.com.es/. En este caso, los ficheros fuente
(aquellos que contienen el programa que se quiere ejecutar) deben tener la extensión .c . Automáticamente, este
compilador genera el fichero ejecutable, con el mismo nombre que el fichero fuente, pero con la extensión .exe .
La generación del fichero objeto o los ficheros objeto y el proceso de enlazado se hace automáticamente y pasa de
forma inadvertida para el usuario.
1.6. Programación
La programación es el procedimiento por el cual ciertos problemas son transformados de manera que puedan
ser resueltos mediante una computadora. Este procedimiento puede descomponerse, grosso modo, en dos partes:
algoritmo y lenguaje de programación. Para realizar un programa que resuelva un problema mediante un proce-
dimiento computacional es importante que se tenga claro primeramente, cuál es el proceso esquemático que debe
seguirse para resolverlo y segundo, la implementación de dicho algoritmo en un lenguaje que sea entendible por
el ordenador.
Un concepto más abstracto y formal de programa se puede encontrar en Wirth [20]. Expresa que los programas
deben estar apoyados en algoritmos eficientes y en un adecuado diseño de datos para poder dar lugar resoluciones
óptimas de los problemas planteados. El diseño de los datos capaces de representar adecuadamente las ideas del
algoritmo desembocan en las estructuras de datos, esto conlleva a considerar lenguajes capaces de implementar
estas estructuras.
4
Esta extensión es propia del sistema operativo Windows. En Linux por ejemplo, el fichero de salida tiene como extensión: .out
J. Sanguino
10 Capítulo – 1. Elementos de un ordenador
El concepto de algoritmo como proceso para resolver problemas a través de procesos computacionales surge
con el trabajo de Turing y Church (véase Gil y Quetglás [6, pág. 41]). A partir de este momento la idea algoritmo
se desarrolla de manera más formal en el mundo de los procesos computacionales (véase Knuth [11]).
El algoritmo es el primer paso que debe considerarse en la resolución de un problema mediante medios compu-
tacionales. Su descripción debe ser independiente del lenguaje que se utilice para su posterior codificación. Me-
diante instrucciones simples o sentencias se deben establecer los pasos necesarios para llegar correctamente a la
solución. Estas sentencias deben estar constituidas por las estructuras de datos abstractas, previamente estableci-
das. Pueden existir diferentes algoritmos para un mismo problema, como en matemáticas pueden existir diferentes
soluciones para un mismo problema. Pero al igual que en matemáticas, siempre se intentará buscar el algoritmo
óptimo. No obstante, la idea de algoritmo óptimo genera cierta controversia en el mundo de la computación. Puede
haber algoritmos expresados de forma compacta y sencillos de entender, pero que su ejecución no sea óptima desde
el punto de vista computacional. Por ejemplo, los llamados algoritmos recursivos (que se estudiarán más adelante)
son ejemplos de algoritmos que permiten resolver ciertos problemas de manera muy compacta, pero que en ciertas
ocasiones, las versiones iterativas de esos mismos problemas, sin ser tan claros ni compactos, resultan mucho más
eficientes desde el punto de vista computacional.
Siguiendo la idea de Wirth [20], una vez planteado el algoritmo y con unas estructuras de datos determinadas
se plantea la construcción del programa. La idea es ajustar de manera eficiente las sentencias del algoritmo con las
estructuras de datos establecidas.
Durante la segunda mitad de los años 50 y comienzos de los 60 ya comenzaba a desarrollarse de manera
significativa los procesos computacionales. Se tenía claro el concepto de algoritmo, pero no tanto las ideas sobre
las estructuras de datos necesarias para implementar estos algoritmos. Así pues, en los primeros lenguajes de
programación de ámbito popular (COBOL, FORTRAN) las codificaciones resultaban oscuras y complicadas, sobre
todo cuando diferentes programadores debían trabajar sobre una misma codificación. Resulta clave la claridad en
la implementación del algoritmo. Esto es una realidad para todo aquel que comience en el mundo de la Informática.
Es fundamental que el programa que se realice sea claro, no sólo para la persona que lo programa, sino para todo
aquel que en cierto momento necesite consultarlo. Así pues, en este sentido para eliminar estos inconvenientes
aparecieron nuevas estructuras de datos que a su vez, generó nuevos planteamientos y teorías sobre la metodología
de programación.
En este curso se tratará de seguir el modelo de programación estructurada. Es deseable que todo programa
que se realice cumpla con los siguientes objetivos (véase Gil y Quetglás [6, pág. 46])
3. Debe ser fácil de mantener (ampliar con nuevas especificaciones o modificar las ya existentes).
La idea de seguir una metodología estructurada es precisamente conseguir los objetivos anteriores. Se trata de
plantear programas jerárquicos o con metodología top–down. Para ello nuestro objetivo será dividir el programa
en subprogramas más sencillos y que el flujo del programa no implique volver a sentencias anteriores. Por ello
comandos del tipo goto que existían en las primeras versiones de los lenguajes de programación y permitían
desviar el flujo de la ejecución a cualquier parte del código, quedan desterradas y prohibidas.
Para implementar esta metodología es necesario disponer de lenguajes que soporten estas especificaciones. Con
el desarrollo de los métodos computacionales las versiones de los lenguajes existentes y los que fueron apareciendo
se adaptaron a estos requisitos. En la actualidad, prácticamente casi todos los lenguajes se adecúan a esta forma de
programación estructurada, en particular, el lenguaje C que es el objeto de este curso de programación.
No obstante, hay que destacar que con el desarrollo de los procedimientos computacionales han surgido nuevos
lenguajes capaces de abordar problemas cada vez más globales. Los lenguajes C, FORTRAN , COBOL , PASCAL son
ejemplos de lenguajes procedimentales. Es decir, cada sentencia o instrucción obliga al compilador que realice
alguna tarea: obtén un dato, escribe esta frase, divide dos números, . . . En resumen, puede decirse que un programa
escrito en un lenguaje procedimental es un conjunto de instrucciones o sentencias. Esto no es óbice, para que en
caso necesario se puedan considerar subprogramas o funciones que simplifiquen el procedimiento de resolución.
J. Sanguino
1.6 – Programación 11
En cualquier caso, para problemas no muy complejos, estos lenguajes tienen unos principios de organización
(llamado paradigma) que son eficientes: el programador sólo tiene que crear una lista de instrucciones en este tipo
de lenguajes, compilar el programa en el ordenador que ejecutará las órdenes establecidas.
Cuando los problemas se vuelven más complejos y los programas más grandes, la necesidad de modularizar
es clara. Es obligado, como ya se ha comentado, definir funciones, procedimientos, subrutinas o subprogramas
que simplifiquen la presentación y sirvan para otros programas. La mayor parte de los lenguajes procedimentales
actuales se ajustan a esta necesidad (en particular el C ). No obstante, el principio sigue siendo el mismo: agrupar
componentes que ejecutan listas de de instrucciones o sentencias. Esta característica hace que a medida que los
programas se hacen más complejos y grandes, el paradigma estructurado comienza a no ser adecuado, resultando
difícil terminar los programas de manera eficiente. Existen al menos, dos razones por las que la metodología es-
tructurada se muestra insuficiente para abordar problemas complejos. Primeramente, las funciones tienen acceso
limitado a los datos globales y segundo, las funciones inconexas y datos, fundamentos del paradigma procedimen-
tal, proporcionan un modelo pobre del mundo real.
En este sentido surgen los lenguajes orientado a objetos, que siguen un paradigma de programación más
adecuado para la resolución de problemas complejos más ajustados a la realidad. La idea, entre otras muchas, es
diseñar formatos de datos que se correspondan con las características esenciales del problema. Se trata de combinar
en una única unidad o módulo, tanto datos como las funciones que operan con estos datos, hablándose de objeto.
Las funciones de un objeto se llaman funciones miembro o métodos y son el único medio para acceder a sus datos.
Los datos de un objeto también se conocen como atributos o variables de instancia. No se puede acceder a los
datos directamente. Los datos son ocultos, de modo que están protegidos de alteraciones accidentales. Los datos y
funciones se dicen que están encapsuladas en una única entidad. El encapsulamiento de datos y la ocultación de
los datos son términos clave en la descripción de los lenguajes orientados a objetos. Otras características son
Herencia.
Polimorfismo.
El C++, Java son ejemplos de lenguajes orientado a objetos. Con respecto al C, resulta que el C++ es más
flexible porque tiene además, capacidades procedimentales, que coinciden con el propio C, por ello se dice que el
lenguaje C es un subconjunto del C++.
J. Sanguino
CAPÍTULO 2
2.1. Introducción
Tengo la convicción, como consecuencias de los muchos años que llevo dedicado a la programación, que
para iniciarse en esta disciplina es necesario y recomendable proporcionar, exclusivamente, unas primeras ideas
básicas. Esto permitirá al alumno pensar y desarrollar algoritmos elementales que le conduzcan a asentar unos
procedimientos de trabajo, que serán fundamentales para abordar conceptos cada vez más complejos y generar
programas a su vez, más sofisticados.
Este capítulo pretende proporcionar estas ideas básicas necesarias de programación. El procedimiento será
introducirlas mediante tres sencillos programas, codificados en C y que se analizarán detalladamente. El primero
de ellos, trata de mostrar las partes en las que se divide un código. En el segundo, se introduce la declaración de
variables, junto con una estructura de selección. En el último, se define una estructura de repetición. Así pues,
con:
1.- la declaración de variables.
2.- una estructura de selección y
3.- una estructura de repetición
ya pueden codificarse una gran cantidad de problemas elementales en los que se plantean conceptos y procedi-
mientos que son esenciales para aprender a iniciarse en la programación de manera autónoma.
13
14 Capítulo – 2. Comenzando con la programación
y cuyo resultado en la pantalla, una vez compilado y ejecutado el fichero fuente (llamado saludos.c), es el
siguiente:
Hola mundo!
Este es mi primer programa en C
-------------------------------------------
Process exited after 0.4095 seconds with return value 0
Presione una tecla para continuar . . .
Línea 1. Esta primera sentencia está diciendo que se debe incluir el fichero stdio.h, que contiene unas defini-
ciones de funciones de entrada salida (en particular printf()), de ahí su nombre: standard input output
header. El símbolo # la identifica como línea a ser manipulada por el preprocesador C. Tal y como se infiere
por el nombre, el preprocesador realiza algunas tareas antes de comenzar la compilación.
Línea 2. Se trata de un comentario. Todo aquello que esté entre /* y */ lo ignora el compilador. El texto entre
estos dos símbolos puede ocupar, incluso varias líneas. Los comentarios son notas que se introducen para
hacer más claro el programa.
Línea 3. Indica que main es una función de tipo entero (ya se estudiará más adelante qué quiere decir esto) y
que no tiene argumentos, por eso aparecen dentro del paréntesis: (void). Los programas en C se suelen
componer de una o más funciones. Esta declaración expresa que se trata de la función principal. Todo
programa en C comienza su ejecución siempre con la instrucción que lleva el nombre de main. Todas las
demás funciones podrán llevar el nombre que elijamos nosotros, pero la existencia de la función main es
obligatoria. Todo aquello que queda antes de la función main se le denomina cabecera del programa.
Línea 4. La llave { indica que comienza la definición de la función (en este caso, de la función main). La
definición acaba en la LÍNEA 9 con el carácter } .
Línea 5. Se trata de otra función. En este caso de salida de información, que por defecto interpreta que es la
pantalla del ordenador. Imprime la frase comprendida entre comillas, que es lo que constituye su argumento.
Dentro de las comillas está el símbolo \n que simplemente es la instrucción de: comienza una nueva línea
ajustándote al margen izquierdo. Forma parte de los comandos denominados secuencias de escape.
Un detalle fundamental es el punto y coma ; al final de la instrucción. Es la manera de indicar al C que el
instrucción ha terminado. Es un símbolo que es parte de la sentencia y no simplemente un separador, como
puede ocurrir en otros lenguajes. Por tanto, su utilización al final de cada sentencia ejecutable es obligatoria.
J. Sanguino
2.4 – Segundo programa 15
Línea 8. Le indica al compilador que ha terminado la descripción de la función main y por tanto, debe terminar
la ejecución del programa devolviendo un valor 0, como resultado de una terminación normal. Este valor
aparece después, en la pantalla de salida de ejecución del programa cuando pone: [. . . ] with return
value 0. No obstante, en este documento no se prestará mucha atención al resultado de la ejecución que
aparece después de la línea discontinua y por tanto NO se mostrará en los resultados de ejecución de los
programas que se realicen.
Pseudocódigo Bloque si
si (condición) entonces
Sentencias (V)
fin-si
Las instrucciones de C asociadas a ellas son if e if-else, respectivamente y cuya descripción gráfica, mediante
un diagrama de flujo puede verse en la figura 2.1. Como puede observarse, con este tipo de instrucción se puede
F V F
condición condición
if ( condición ){ if ( condición ){
V sentencias (V); sentencias (V);
} } else {
sentencias sentencias (V) sentencias (F) sentencias (F);
sentencias (V) }
sentencias;
sentencias
sentencias
J. Sanguino
16 Capítulo – 2. Comenzando con la programación
1. la suma,
2. el producto,
3. el promedio. Para ello, la suma se divide por 3. Si este resultado no es entero se calcula el cociente entero y
su resto. Por último,
27 promedio = suma/3;
28 resto = suma % 3;
29 if( resto != 0 ){
30 printf("El promedio es %i y su resto %i\n", promedio, resto);
31 } else {
32 printf("El promedio es %i \n", promedio);
33 }
34
35 if(entero1 > entero2){
36 aux = entero1;
37 entero1 = entero2;
38 entero2 = aux;
39 }
40 if(entero2 > entero3){
41 aux = entero2;
42 entero2 = entero3;
J. Sanguino
2.4 – Segundo programa 17
43 entero3 = aux;
44 }
45 if(entero1 > entero2){
46 aux = entero1;
47 entero1 = entero2;
48 entero2 = aux;
49 }
50
51 printf("\n Los números enteros ordenados son: %i, %i, %i\n\n", entero1,
entero2, entero3);
52
53 return 0;
54 }
J. Sanguino
18 Capítulo – 2. Comenzando con la programación
está formado por especificaciones de formato que se ponen entre comillas (" "). En este caso: %i %i %i,
indican que se van a introducir tres números enteros2 . La lista de argumentos coincide con el nombre de las
respectivas variables, separadas por comas y precedido cada una de ellas, por el símbolo & . Esto indica a la
función scanf la dirección de las respectivas variables. Este asunto se desarrollará posteriormente (véase
subsección 3.2.3, pág. 40).
Líneas 15-17. Se genera un eco de los datos de entrada. El motivo es comprobar que la lectura de los datos intro-
ducidos se ha realizado correctamente. La función printf, al igual que sucedía con el comando scanf, se
compone de una cadena de control y una lista de argumentos. En la cadena de control se pone entre comillas
(" ") el texto que aparecerá en la pantalla, con los especificadores de formato necesarios, correspondientes
al tipo de variables que constituyen la lista de argumentos, cuyas variables, de nuevo, deben estar separadas
por comas. En el texto del ejemplo, se introducen, de nuevo, los especificadores %i debido a que en la lista
de argumentos, sólo hay variables enteras. En este caso, únicamente se especifica el nombre de la variable
sin estar precedido del símbolo & .
Líneas 21, 24, 27 y 28. En estas líneas se realizan operaciones aritméticas elementales enteras. Aunque la des-
cripción de todas ellas es obvia se tratarán más adelante (véase subsección 3.4.2, pág. 52). Sin embargo,
merece la pena resaltar la palabra entera. Hace referencia a que los datos utilizados para operar son enteros
y por tanto, el resultado también será también entero. Así pues, en la división
promedio = suma/3;
puesto que suma es una variable entera y 3 es una constante entera, la operación se realiza con enteros y
el resultado es por tanto, entero. Esto quiere decir que si suma no almacena un múltiplo de 3, entonces
el resultado que se almacene en la variable promedio será únicamente, la parte entera del resultado. Por
último, señalar que la operación
resto = suma % 3;
Líneas 29-33. Se trata de un bloque if-else que permite cambiar el flujo del programa, de acuerdo con el
resultado de la condición, que se encuentra entre los paréntesis que siguen a la instrucción if. En este caso,
se pregunta si resto es distinto a 0 (resto 6= 0), que en C se escribe como
resto != 0
En caso de ser cierta, se ejecutan todas las sentencias del bloque if, esto es, todas las sentencias que están
a continuación del comando if, que están entre llaves. En este programa, sólo existe una sentencia:
Una vez ejecutada, el flujo del programa sigue con la siguiente sentencia ejecutable, que se encuentra a
continuación de la llave que cierra el bloque else. En el ejemplo, iría a la LÍNEA 35 del programa. Si la
evaluación de la condición es falsa, entonces el flujo del programa se salta el bloque if y realiza el bloque
else. Esto quiere decir, que ejecuta todas las sentencias que están a continuación de la instrucción else y
están entre llaves. En este ejemplo, sólo se ejecutaría la sentencia:
2
También es posible utilizar el formato %d para valores enteros. Este formato era obligatorio para los valores enteros en versiones
clásicas de C. Sin embargo, resultaba poco intuitivo, por ello ahora es posible utilizar el formato %i, para el tipo int.
J. Sanguino
2.5 – Tercer programa 19
Una vez terminado, el flujo del programa sigue en la sentencia que se encuentra después de la llave que
cierra el bloque else. En el programa anterior, sería la LÍNEA 35.
Para construir las expresiones que constituyen las condiciones, que deben ser evaluadas para determinar la
dirección del flujo del programa, se suelen construir mediante los operadores de relación y los operadores
lógicos. Para conocer distintos tipos de estos operadores y sus propiedades, puede consultarse las subseccio-
nes 3.4.5 (pág. 56) y 3.4.6 (pág. 57), respectivamente.
Líneas 35-39. Se tratan de un bloque if. Su descripción es similar al caso anterior, del bloque if-else. Se
evalúa la condición, que está a continuación de if y entre paréntesis. Si es cierta, entonces se ejecutan todas
las sentencias que están entre las llaves. En este programa, en caso de ser cierta la condición, se ejecutarían
las sentencias:
aux = entero1;
entero1 = entero2;
entero2 = aux;
Una vez terminado, el flujo del programa continúa con la siguiente sentencia después de la última llave. En
este caso, seguiría en la LÍNEA 40. Si la condición es falsa, entonces el flujo del programa, se salta el bloque
de instrucciones y continúa con la siguiente sentencia ejecutable que sigue a la última llave del bloque. En
este ejemplo, la LÍNEA 45.
Básicamente estos algoritmos describen un intercambio de valores entre dos variables, que puede verse con
más detalle en la sección 3.2 (pág. 27).
Líneas 40-44, 45-49. De nuevo se tratan de bloques if y su interpretación es similar al caso anterior ya descrito.
Ejercicios:
1. Realiza un programa que lea un número del teclado y calcule su valor absoluto.
2. Escribe un programa que lea dos números del teclado e imprima el máximo de ambos y su promedio.
3. Realiza un programa que lea por teclado el número de segundos que dura cierto suceso y muestre su valor en horas,
minutos y segundos.
N
J. Sanguino
20 Capítulo – 2. Comenzando con la programación
F
condición
while(condición){
V sentencias (V);
}
sentencias (V)
J. Sanguino
2.6 – Buenas prácticas para realizar un programa 21
Líneas 21-25. Se trata de un bloque if-else, similar al ya comentado anteriormente. Sin embargo, en la con-
dición aparece el operador de relación == . Esta instrucción comprueba si los valores que están en sus
extremos son iguales o no. Es importante no confundir este operador con el de asignación: = , que como ya
se ha comentado, introduce el valor que está a su derecha, en la variable que se encuentra a su izquierda.
El resto de las sentencias son muy similares a las ya estudiadas en los programas anteriores y no se vuelven a
describir.
Ejercicios:
1.– A la vista de los ejemplos anteriores, si parte de un código en C contiene las siguientes sentencias
........
........
a = 5;
b = -3;
b = a;
a = b;
......
.......
1. deben elegirse nombre de variables que sean aporten cierto significado. Si se utiliza una variable como
contador, tiene más sentido llamarla cont que xz3.
2. deben usarse comentarios para indicar que es lo que se va a realizar y qué se está resolviendo.
3. deben emplearse líneas en blanco para separar diferentes secciones en los programas: explicación del ob-
jetivo del programa, declaración e inicialización de variables, entrada de datos, algoritmo, salida de datos,
etc
J. Sanguino
22 Capítulo – 2. Comenzando con la programación
4. se debe utilizar una línea por sentencia. El C tiene la particularidad de ser un lenguaje de formato libre
(free-form format). Esto implica que está permitida una codificación del tipo
int main(void)
{
int prueba; //Declaración
prueba = 9; //Inicialización
return 0;
}
5. y...sobre todo sigue el mismo modelo de codificación que se siguen en la multitud de ejemplos que contiene
este documento.
2.7. Problemas
Todos ejercicios que se plantean pueden resolverse a partir de los tres programas analizados en este capítulo,
utilizándolos como modelo.
entero = 12;
if(entero = 7){
printf("El valor de entero es 7);
} else {
printf("El valor de entero es 12);
}
cont = 1;
while (cont <= n){
cont = cont + 1;
J. Sanguino
2.7 – Problemas 23
2. Dada la sentencia
resultado = a * b * c;
Escribe un programa que pida al usuario tres números enteros, utilice la sentencia anterior y escriba por
pantalla el resultado.
3. Escribe un programa que pida un número entero y determine si es par o impar. Una ejecución típica debería
ser:
Ahora haz otro programa que determine si el número entero es múltiplo de tres. Si no lo es, que determine
su resto. Una ejecución típica del programa debe ser:
4. Siguiendo los problemas anteriores, ahora se pide que escribas un programa que pida un número entero y
a continuación otro entero. Que compruebe si éste último es un divisor del primero. En caso contrario que
determine el resto, de la división entera. Una ejecución típica del programa debe ser:
5. Dados dos números enteros M y N determina la cantidad de números pares en el intervalo (M, N ).
6. Escribe un programa que intercambie los respectivos valores que contengan dos variables. Esto es, si la
variable a = −12 y la variable b = 7, entonces el código tiene que ser capaz de obtener la salida: a = 7 y
b = −12. Una posible ejecución podría ser:
J. Sanguino
24 Capítulo – 2. Comenzando con la programación
var1 = 1;
var3 = 1;
var2 = 25;
while (var1 < var2){
var1 = var1 + 1;
var3 = var3 * var1;
}
8. Escribe un programa que calcule el factorial de número natural, dado. Una posible ejecución sería:
El factorial de 7 es 7! = 5040
9. Escribe un programa que calcule la potencia entera positiva de un número entero. Esto es, si a ∈ Z es la
base y b ∈ N el exponente, se pide ab . Una posible ejecución sería:
La potencia 5 de -2 es -32
10. Dado un número natural, determina el dígito mayor. Una posible ejecución del programa sería:
J. Sanguino
2.7 – Problemas 25
11. Escribe un programa que sume los dígitos de un número natural. Esto es, dado el número 7435 la suma de
sus dígitos es 19. La idea es ir obteniendo el resto de la división entera por 10. Por ejemplo, siguiendo con
el número anterior: 7435 % 10 = 5 y 7435/10 = 743. Seguidamente se hace el mismo proceso pero ahora
con 743. Es decir: 743 % 10 = 3, que será otro dígito que se suma al anterior y se opera: 743/10 = 74. A
continuación se sigue una pauta análoga con 74 y así sucesivamente, hasta que la parte entera sea cero. Una
posible ejecución del programa sería:
12. A partir del programa anterior, escribe un código que escriba el reverso de un número natural. Esto es, si
se introduce el número 45178, la salida debe ser 87154. A continuación que compruebe si es capicúa o no.
Una posible ejecución del programa sería
13. Escribe un programa que determine si un número es perfecto. Se dice que un número natural es perfecto si
su valor coincide con la suma de sus divisores. Por ejemplo, el número 6 es perfecto ya que 6 = 1 + 2 + 3.
También lo son 28 y 496. La idea del algoritmo es muy sencilla: basta con tomar todos los números menores
que el introducido y comprobar si alguno es divisor. Si lo es, entonces se suma con los anteriores que han
sido divisores. Al final la suma total y el número natural inicial deben ser iguales, si el número es perfecto.
Utilizando el código anterior, haz un programa que dado un número natural, saque por pantalla todos los
números perfectos menores que él.
14. Haz un programa que calcule el Máximo común divisor (m.c.d.) y el Mínimo común múltiplo (m.c.m.) de dos
números naturales.
NOTA:
Para calcular el máximo común divisor (m.c.d.) de dos números enteros positivos, puede aplicarse
el Algoritmo de Euclides. Este proceso se basa en que dados dos números D (dividendo) y d
(divisor) con D > d, la división entre ambos números se expresa de la forma:
D =C ·d+r
donde C es el cociente y r el resto. Si k es un divisor de D y d, entonces estos valores serán de la
forma: D = T · k y d = s · k y como consecuencia, k también debe ser un divisor de r, ya que:
T · k = C · s · k + r ⇐⇒ k · (T − C · s) = r
J. Sanguino
26 Capítulo – 2. Comenzando con la programación
Teniendo en cuenta este hecho, se hace a continuación, la división entre d y r, pero ahora d como
dividendo y r como divisor, obteniéndose:
d = C2 · r + r2
D <- a; d <- b;
mientras (r != 0) realiza
r <- mod(D,d) //resto de la división entera
D <- d
d <- r
fin-mientras
el resultado es: D
Como ejemplo y antes de resolver el problema aplica este algoritmo, para determinar el m.c.d.(35, 25).
J. Sanguino
CAPÍTULO 3
Introducción al lenguaje C
3.1. Introducción
En este capítulo se introducen nuevas herramientas y conceptos básicos del lenguaje C, que permitan desarro-
llar unos programas más completos y eficientes.
3.2. Datos
En un programa se gestionan dos modelos de datos: constantes y variables. Ambos se caracterizan por alma-
cenar valores en la memoria de un ordenador, que se accede a través de una dirección numérica. La diferencia
entre los dos tipos de datos consiste en que el valor de las constantes no puede modificarse durante la ejecución de
programa y el de las variables sí.
Que una variable pueda tomar valores diferentes a lo largo de la ejecución de un programa hace que se asemeje
al concepto de variable estudiada en Matemáticas. Sin embargo, esta similitud cambia, cuando por ejemplo, se
quieren intercambiar los valores de dos variables. Mientras que matemáticamente el proceso es inmediato, desde
un punto de vista informático, ya no lo es. Esto es debido a las posiciones de memoria (direcciones) que ocupan los
valores de cada una de las variables. Para poder modificar su contenido es necesario un proceso de asignación de
valores. Como consecuencia, si se quiere intercambiar el valor de dos variables, es necesario involucrar una tercera
variable auxiliar (véase figura 3.1) Esta idea ya se utilizó en el capítulo anterior (véase el código 2.2, pág. 16). A la
Var1 3
1376 −235
Aux
−235
2
Var2
-235 1376
Figura 3.1 – Intercambio de los valores de dos variables. En negro se muestra la idea de intercambio, en rojo el procedi-
miento de intercambio de variables en Informática: en los círculos se determina el orden de las etapas.
vista de este ejemplo, es claro que la variable informática, tiene cierta similitud con la variable matemática, pero
no es lo mismo.
Es básico insistir en que los datos en Informática ocupan un espacio físico en la memoria, identificado por una
dirección (que suele ser un número). Para que el programa pueda realizar de manera eficaz el acceso a los valores
27
28 Capítulo – 3. Introducción al lenguaje C
Tabla 3.1 – Algunos tipos básicos de variables en C. Los tamaños que se muestran se han realizado con el compilador
Dev–C++ Ver. 5.11
de un dato, la memoria se organiza en grupos de bytes que son direccionables (esto es, cada byte tiene un número),
tal y como se especificó en la sección 1.3 (pág. 6). Con este planteamiento, para utilizar un dato en un programa,
es necesario
1. hacer saber al programa que quieres utilizar una porción de su memoria para almacenar un valor (en principio
nos da igual en qué dirección de memoria) y además indicar qué tipo de valor se va a utilizar: si es tipo
carácter, de tipo entero o real1 (hay algunos tipos de variables un poco más sofisticados, pero se verán más
adelante)
2. así mismo, se le puede comunicar que nos interesa introducir un valor en esa dirección de memoria, que ha
reservado anteriormente (esto no es imprescindible, pero es una buena costumbre a la hora de programar).
Estos dos pasos se llaman declaración e inicialización de variables, respectivamente. Veamos cada uno de ellos
más detenidamente
Declaración. Este paso es obligatorio en C y se le dice al ordenador, qué tipo de dato se va a emplear y cuánto
espacio de memoria se necesita, junto con la manera de identificarla. El tipo y el espacio se realiza en una
misma etapa, de esta forma para realizar una sentencia de declaración se utilizan una serie de palabras
claves que se muestran en la tabla 3.1 (el significado de cada una ellas se verá más adelante). Para identificar
el dato se utiliza un nombre alfanumérico (con alguna restricción que se verá más adelante). A continuación
el programa asocia una dirección de memoria a ese nombre elegido por el usuario.
Los tamaños (el número de bytes) asignado a cada tipo de variable, depende del modelo del compilador, o si
funciona bajo 32 o 64 bits. Para poder determinar el tamaño para cada caso, el C ofrece la función sizeof,
1
Hay que advertir, que en Informática los números reales, coinciden más con la idea de números racionales (Q) estudiados en Matemá-
ticas. En Informática no existen los números reales (R) tal y como se entienden en Matemáticas, aunque se hable de ellos.
J. Sanguino
3.2 – Datos 29
que da el tamaño en bytes del tipo de variable. De esta manera, para obtener el cuadro 3.1 se ha hecho un
programa con sentencias parecidas a:
Existe la posibilidad de declarar una variable como long double, aunque ANSI C no garantiza el rango y
precisión mayores que las de double. Generalmente la posibilidad de utilización depende del compilador
y del tipo de procesador (sea 32 o 64 bits). La declaración de estas variables es similar a los casos anteriores
El compilador DEV- C ++ asigna un tamaño de 12 o 16 bytes a este tipo de variables, según se trabaje en 32
o 64 bits, respectivamente. En el presente curso, no se utilizará este tipo de variables.
Ejercicio:
Escribe un programa sencillo de manera que a través de la sentencia sizeof, permita obtener el tamaño de los tipos
de variables, definidas en la tabla 3.1. N
Inicialización. Este paso es opcional para las variables (aunque muy conveniente a la hora de programar), pero
obligatorio para las constantes. La inicialización de un dato se realiza mediante una orden llamada asigna-
ción. Simplemente consiste en que se asigna o deposita en el espacio de memoria que tiene la variable, un
valor. En C este proceso se realiza mediante el símbolo de igualdad: = . Esta notación es un tanto confusa,
porque no tiene nada que ver con el concepto de igualdad en Matemáticas. De hecho, hay lenguajes utilizan
otra notación más gráfica para distinguirlo, por ejemplo: ‘<-’ o ‘:=’. En cualquier caso, en C quiere decir
que el valor que hay a la derecha de = , se asigna al dato que hay a la izquierda. Esto lleva a que tenga
perfecto sentido, en el caso de las variables, sentencias del tipo:
Contador = Contador + 1;
Simplemente indica que se sume una unidad al valor que tiene almacenada la variable Contador, y el
resultado se coloque en la misma posición de memoria, obteniéndose un incremento en una unidad, en el
valor de la variable Contador.
Seguidamente se muestran unos ejemplos de inicialización y declaración de datos
area = 15.734562;
carac = ’a’;
ter = 70; /* Código ASCII de la letra F */
J. Sanguino
30 Capítulo – 3. Introducción al lenguaje C
13 area = 15.734562;
14 carac = ’a’;
15 ter = 70; /* Código ASCII de la letra F */
16
17
18 printf("El valor de carac es -> %c \n", carac);
19 printf("El valor de ter es -> %c \n", ter);
20 printf("El valor de entero es -> %d \n", entero);
21 printf("El valor de flotador es -> %f \n", flotador);
22 printf("El valor de area es %f -> \n", area);
23 printf("El valor de Pi es -> %f \n", Pi);
24 printf("El valor de Pi es -> %.20f \n", Pi);
25 printf("El valor de real es -> %.8f \n", real);
26 printf("El valor de realDoble es -> %.8f \n", realDoble);
27
28 return 0;
29 }
En la LÍNEA 11 se ha utilizado el cualificador const. Esto implica que esta variable no puede cambiar de valor
durante la ejecución del programa. De hecho, se genera un error de compilación si se detecta un comando del tipo
Pi = Pi + 1;
3.2.1. Tipos
En este apartado se pretende analizar más detenidamente los tipos básicos que ofrece el C para los datos. Como
ya se ha dicho, en la declaración de tipos se establece de manera implícita el tamaño de bytes, que ocupará cada
dato en la memoria.
J. Sanguino
3.2 – Datos 31
Tipo char
Aunque por su denominación y uso hace referencia a caracteres (véase el código 3.1), la realidad es que C
los trata como variables enteras de 8 bits (como puede comprobarse mediante el comando sizeof(char),
anteriormente comentado). El hecho de que se pueda manipular caracteres con este tipo de variables, no quiere
decir que almacene los caracteres físicamente. Realmente almacena un número, que corresponde al código ASCII2
de ese carácter. Con 8 bits, una variable del tipo char tiene capacidad para almacenar 256 caracteres (28 = 256).
En el programa 3.1 se utiliza este tipo de declaración:
Líneas 14-15. Se inicializan las variables. Mientras que para la variable carac se realiza explícitamente con el
carácter a , para la variable ter se utiliza el código ASCII, de la letra F.
Líneas 18-19. Se imprime en pantalla los caracteres a y F que son los valores que contienen las variables
carac y ter, respectivamente. Como se observa se utiliza el especificador de formato: %c para impri-
mir los caracteres. Se podía haber utilizado un especificador de formato entero: %i o %d. En este caso, el
resultado hubiese sido el código ASCII del carácter.
Se trata de una variable carácter sin signo. Pero ¿qué quiere decir esto?. Como ya se ha comentado, el lenguaje C
trata a las variables carácter como variables enteras de 8 bits. Que sea una variable carácter sin signo significa que
almacena números enteros del 0 al 255 y por supuesto que estos valores los asigna a símbolos ASCII. Sin embargo,
una variable declarada como char se asocia a una variable entera con signo. Esto quiere decir que su rango va
desde −128 a 127, pero también asigna unívocamente todos estos valores a los signos del código ASCII. A la vista
de esta situación, veamos cómo realmente se representan este tipo de variables internamente:
unsigned char Para este tipo de variables se dedican 8 bits de memoria, por tanto se pueden representar
256 = 28 números que van desde el 0 al 255. La representación interna se realiza en base 2. De esta forma,
el número representado en la figura 3.2(a), sería el número 23 en base decimal
0 0 0 1 0 1 1 1 1 1 1 1 1 1 1 1
7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0
Numeración de los bits Numeración de los bits
0 · 27 + 0 · 26 + 0 · 25 + 1 · 24 + 0 · 23 + 1 · 22 + 1 · 21 + 1 · 20 = 23
El mayor número sería el representado en la figura 3.2(b) y cuyo valor decimal viene dado por3
7
X 1 − 28
1 · 27 + 1 · 26 + 1 · 25 + 1 · 24 + 1 · 23 + 1 · 22 + 1 · 21 + 1 · 20 = 2i = = 28 − 1 = 255
1−2
i=0
El menor sería cuando todos los bits fuesen cero, que obviamente correspondería al valor decimal, nulo. Por
tanto, el rango de las variables declaradas como unsigned char sería [0, 255], como puede verse en el
cuadro 3.1 (pág. 28).
2
Es el acrónimo de American Standard Code for Information Interchange que corresponde a una tabla con símbolos estandarizados
utilizados por los computadores.
3
Utilizando la expresión de la suma de una progresión geométrica
J. Sanguino
32 Capítulo – 3. Introducción al lenguaje C
char Al igual que en el caso anterior, se dedican 8 bits para la representación del valor de la variable. Sin
embargo, en este caso hay un bit que se dedica al signo. Habitualmente el más significativo (el que está más
a la izquierda). Si es 0, se toma como positivo y si es 1, se toma como negativo. A partir de este criterio,
existen diferentes maneras de representación de los números enteros con signo. El más habitual y eficiente es
el llamado complemento a dos, que trata el bit del signo como un bit de magnitud. El criterio que establece
el modelo de complemento a dos, es que la representación decimal debe ser:
6
X
γ · 27 + ai 2i
i=0
donde γ ∈ {0, 1} y ai ∈ {0, 1} con i = 0, . . . , 6. Si el número es positivo, el valor γ será 0. Esto implica
que el mayor número positivo, verifica que γ = 0 y sería el representado en la figura 3.3(a) con lo cual su
0 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0
7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0
Numeración de los bits Numeración de los bits
Con esta regla de complemento a dos, el número negativo más pequeño, sería el representado por la figu-
ra 3.3(b) cuya expresión decimal sería:
1 · 27 + 0 · 26 + 0 · 25 + 0 · 24 + 0 · 23 + 0 · 22 + 0 · 21 + 0 · 20 = (−)27 = −128,
que es negativo porque γ = 1. Para obtener la expresión binaria de los números negativos, el método
complemento a dos toma el valor positivo en binario, calcula su opuesto (intercambia ceros por unos y
viceversa) y suma una unidad. Por ejemplo, para determinar el valor −2 en binario, se tomaría el valor 2 en
binario, que se representa en la figura 3.4(a) y se determina su opuesto, como muestra la figura 3.4(b) y por
0 0 0 0 0 0 1 0 1 1 1 1 1 1 0 1
7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0
Numeración de los bits Numeración de los bits
(a) Valor 2 en binario para una variable (b) Opuesto binario de 2 para una va-
char riable char
último se le sumaría 1, con lo que obtendría el valor −2 en binario, que se muestra en figura 3.5
1 1 1 1 1 1 1 0
7 6 5 4 3 2 1 0
Numeración de los bits
Figura 3.5 – Valor en binario de −2 con criterio complemento a dos para una variable char
J. Sanguino
3.2 – Datos 33
Así pues, el rango de las variables de tipo char estaría en el intervalo [−27 , 27 − 1], como muestra el
cuadro 3.1 (pág. 28).
El lenguaje C proporciona instrucciones para determinar el rango de este tipo de variables carácter de manera
directa, tal y como muestra el código 3.2. En este caso es necesario añadir un nuevo fichero en el encabezamiento:
limits.h
Un ejemplo interesante para visualizar la consecuencia de almacenar un carácter en una variable tipo unsigned
char o en una de tipo char, lo muestra el siguiente programa. El objetivo consiste en imprimir los códigos ASCII
de las variables carácter.
Inicialmente se imprime el código ASCII de un carácter almacenado en una variable unsigned char4 :
Sin embargo, si se cambia el tipo de la variable que almacena el carácter, por un char, entonces se obtendría
4
Este código utiliza funciones y sentencias que se explicarán más adelante. El objetivo es fijarse en el resultado que se obtiene.
J. Sanguino
34 Capítulo – 3. Introducción al lenguaje C
La razón es que el código ASCII de ñ es 164 y una variable char tiene un rango, como se ha visto anteriormente
en el intervalo [−128, 127], con lo cual 164 6∈ [−128, 127], pero sí está incluido en el intervalo [0, 255] de las
variables del tipo unsigned char.
Tipo int
Esta declaración ya se ha utilizado no sólo en el programa 3.1, sino también en los códigos 2.2 (pág. 16) y
2.3 (pág. 20). Hace referencia a una variable entera con signo, que ocupará 4 bytes. De manera similar a lo que
sucedía con el tipo char hay una opción sin signo: unsigned int. La forma de almacenar internamente estas
variables es análoga al caso de las variables unsigned char y char ya vistas. La diferencia es que ahora se
utilizan 4 bytes para almacenar los datos, en lugar de 1 byte, empleado en el caso anterior. Por tanto para variables
del tipo int el rango de variación sería [−231 , 231 − 1] y en el caso de unsigned int: [0, 232 − 1], como puede
comprobarse en el cuadro 3.1 (pág. 28).
Existen otros tipos de variables enteras, que se emplean según el número de bytes que se necesiten en un pro-
grama. Así el tipo short int y unsigned short int utilizan 2 bytes, long int y unsigned long
int 4 bytes, los mismos que int y unsigned int. Por último, long long int y unsigned long
long int reservan 8 bytes. Los rangos de algunos de ellos pueden consultarse en el cuadro 3.1 (pág. 28). Es
importante señalar que la asignación de bytes por parte de los diferentes tipos para un dato entero, pueden variar
de un compilador a otro, o incluso si se emplea un procesador de 32 ó 64 bits. El C garantiza únicamente que
cualificador short no asigna un mayor número de bytes que int y que long, no asigna un menor número que
int.
Análogamente al caso anterior, a través de instrucciones del C puede obtenerse el rango de variación de los
diferentes tipos, como se observa en el código 3.4
4 int main(void)
5 {
6 printf("El rango de short está entre %i y %i\n", SHRT_MIN, SHRT_MAX);
7 printf("El rango de unsigned short está entre 0 y %u\n\n", USHRT_MAX);
8
9 printf("El rango de int está entre %i y %i\n", INT_MIN, INT_MAX);
10 printf("El rango de unsigned int está entre 0 y %u\n\n", UINT_MAX);
11
12 printf("El rango de long está entre %li y %li\n", LONG_MIN, LONG_MAX);
13 printf("El rango de unsigned long está entre 0 y %lu\n\n", ULONG_MAX);
14
15 printf("El rango de long long está entre %lli y %lli\n",LLONG_MIN, LLONG_MAX);
16 printf("El rango de unsigned long long está entre 0 y %llu\n", ULLONG_MAX);
17
18 return 0;
19 }
J. Sanguino
3.2 – Datos 35
12 return 0;
13 }
la salida en la pantalla es
Por lo general, cuando se utiliza un número en un programa, por ejemplo: 49015, este valor se almacena
como un tipo int. Sin embargo, hay ocasiones que interesa que una constante numérica se almacene como un
entero más grande. Puede ocurrir que sea conveniente considerar el número 3, como un entero de 8 bytes. Para
ello, se utilizan los sufijos ll o LL, indistintamente. Esto es, este valor se expresaría de la forma: 3LL. También es
conveniente distinguir las constantes numéricas que son de tipo entero, de las que son de tipo entero sin signo. Para
indicar que un valor numérico entero debe asociarse a un tipo sin signo se utiliza el sufijo U o u, indistintamente.
Así, el valor 123 se asocia a un tipo int, mientras que 123U se asocia a un unsigned int.
Tipo float
Los datos con este tipo de declaración permiten almacenar valores numéricos con decimales. Un ejemplo
puede observarse en la LÍNEA 7 del programa 3.1 (pág. 30). Las variables flotador y area, se declaran como
float, incluso la primera de ellas es inicializada.
En el mundillo de la Informática a estas variables se les identifica de manera general con el nombre de núme-
ros reales. Pero como ya se ha comentado, están más cerca de lo que se entiende en Matemáticas por números
Racionales (Q) que de los números Reales (R), propiamente dicho. La cuestión es que el compilador no almacena
de la misma forma un número entero, que un número dotado de parte decimal, aunque tengan el mismo número
de bytes asignados. Para los números reales se suele utilizar el formato estándar de representación (IEEE Storage
Format) (véase libro Infante et al [8, pág. 22]).
El hecho de utilizar float para declarar variables indica, no sólo que se almacenarán valores decimales, sino
que además debe reservarse un tamaño de 4 bytes. En efecto, el compilador reserva 4 bytes que equivalen a 32
bits y en estas posiciones se almacena el número en binario, con una representación científica. Esto implica que se
necesita espacio para almacenar el signo, la mantisa y el exponente con su signo. La regla que se considera es que
de estos 32 bits, el primer bit lo utiliza para el signo (0 si es positivo y 1 si es negativo), los 8 bits siguientes los
emplea para almacenar el exponente y los 23 bits últimos para la mantisa, tal y como se representa en la figura 3.6
y que se suele expresar de manera normalizada5 , de la forma:
J. Sanguino
36 Capítulo – 3. Introducción al lenguaje C
donde s es el signo, m es la mantisa (únicamente contiene 0’s y 1’s) y Ed es la expresión decimal del número
binario guardado en los 8 bits dedicados al exponente. Esta manera de representación, permite que el 1 más
significativo de la representación normalizada (3.1) (esto es, el que está a la izquierda del punto), no sea necesario
almacenarlo en m y así, dejar espacio para un bit más (aumentando la precisión).
Por otra parte, para evitar tener que utilizar un bit adicional para el signo del exponente, se establece el siguiente
criterio6 : para almacenar el valor (decimal) correspondiente al exponente y su signo, se suma 127 al exponente y
se utilizan 8 bits para almacenarlo; de manera inversa, el exponente (en decimal) se obtendrá al restar 127 a la
conversión decimal del exponente almacenado (que está en binario). Puesto que hay 8 bits, se pueden almacenar
28 = 256 números (de 0 a 255). Sin embargo, se eliminan el 0 y el 255 (para números especiales), con lo que
quedan 254 números. La representación en base binaria de los valores extremos del exponente serían:
Existen dos conceptos importantes que son precisión y rango, que caracterizan a los distintos tamaños dedi-
cados para almacenar números reales.
Precisión. Hace referencia al número de cifras con los que se representa la mantisa. Para el caso del tipo float
se dedican 23 bits, lo que quiere decir que el mayor número que puede representarse (teniendo en cuenta la
suma de una progresión geométrica) es
22
X 1 − 223
1 · 20 + 1 · 21 + · · · + 1 · 222 = 2i = = 223 − 1 = 8.388.607(10)
1−2
i=0
Esto quiere decir que se pueden representar todos los números decimales de 6 cifras y la mayor parte (aun-
que no todos) de los de 7 cifras (por ejemplo el número 9.231.543 no se puede representar con 23 bits).
Este tipo de cifras también suelen llamarse cifras significativas. Por tanto, se dice que la precisión de esta
representación está entre 6 y 7 cifras significativas.
Rango. Se refiere a los valores que puede ser representados (con la limitación del tamaño de la mantisa), limitados
por la cota superior e inferior (en valor absoluto). Para los números llamados normales (aquellos que tienen
una representación de la forma (3.1)) los valores extremos serían:
Extremo superior. Corresponde a un número positivo almacenado como muestra la figura 3.7
Figura 3.7 – Esquema de almacenamiento interno del máximo número normal positivo de 4 bytes
6
Es el llamado criterio de representación sesgada o codificación en exceso, con sesgo 2k−1 − 1. En este caso k = 128.
J. Sanguino
3.2 – Datos 37
Ahora bien, teniendo en cuenta que la suma para una progresión geométrica de razón 2 es
n−1
X
2i = 2n − 1
i=0
resulta que
127
X
2i = 2128 − 1
i=0
de esta forma:
127
X 103
X 127
X
2i = 2128 − 1 = 2i + 2i
i=0 i=0 i=104
| {z }
α(10)
y despejando resulta
127
X 127
X 103
X
2i = 2i − 2i = 2128 − 1 − 2104 − 1 = 2128 − 2104 = 3.4028234 × 1038
α(10) =
i=104 i=0 i=0
Figura 3.8 – Esquema de almacenamiento interno del mínimo número normal positivo de 4 bytes
Es importante insistir que no es posible representar en el computador todos los números x que cumplan
como consecuencia de la limitación física en el almacenamiento de la mantisa. Este resultado coincide con el
que ya se mostraba en el cuadro 3.1 (pág. 28). Análogamente como en los casos anteriores, se puede determinar
mediante instrucciones C, el rango y las cifras significativas, como muestra el código 3.6
J. Sanguino
38 Capítulo – 3. Introducción al lenguaje C
5 {
6 printf("El rango de un float es de %g a %g\n", FLT_MIN, FLT_MAX);
7 printf("El tipo float proporciona %u dígitos de precisión \n", FLT_DIG);
8
9 return 0;
10 }
Tipo double
Es similar al tipo float, la única diferencia es el número de bytes que se utilizarán para almacenar el valor
numérico. En este caso, se emplean 8 bytes. Como es de suponer, la representación interna es análoga al caso
anterior, únicamente cambia el tamaño para el almacenamiento del exponente y la mantisa. Se sigue empleando
un bit para el signo del número, pero 11 bits para el exponente y 52 bits para la mantisa, tal y como muestra la
figura 3.9
Precisión. Puesto que la mantisa está constituida por 52 bits resulta que el mayor número representable será
51
X
2i = 252 − 1 = 4.503.599.627.370.495
i=0
Rango. Siguiendo un procedimiento análogo al establecido para el caso del tipo float, se llega a que el rango está
entre
2.225073858507201 × 10−308 6 x 6 1.797693134862316 × 10308
que como era de esperar, coincide con el que ya se mostraba en el cuadro 3.1.
Un ejemplo de utilización de este tipo de declaración e inicialización puede verse, de nuevo en el código 3.1
(pág. 30), en la LÍNEA 9. Para obtener una salida por pantalla se utiliza también el especificador de formato: %f,
como puede apreciarse en la LÍNEA 26.
Es interesante además, resaltar la asignación que se hace a la variable Pi en la LÍNEA 11 del programa, con
el resultado que se obtiene con los comandos de las LÍNEAS 23–24. El número de dígitos que se quieren mostrar
en la salida, viene determinado por el formato. En la LÍNEA 23 aparece el símbolo %f, que simplemente indica
que esa posición se va a introducir un número real. El compilador toma por defecto el número de decimales. Sin
embargo, en la LÍNEA 24 el formato es %.20f y con ello se le comunica al compilador de manera explícita, que
deje 20 posiciones para la parte decimal.
Por último, en el programa 3.7 se muestran las instrucciones en C que permiten determinar el rango y la
precisión (o cifras significativas) del tipo double.
J. Sanguino
3.2 – Datos 39
1 #include <stdio.h>
2 #include <float.h> // Límites para los tipos "punto decimal"
3
4 int main(void)
5 {
6 printf("El rango de un double es de %.15g a %.15g\n", DBL_MIN, DBL_MAX);
7 printf("El tipo double proporciona %u dígitos de precisión\n", DBL_DIG);
8
9 return 0;
10 }
Constantes numéricas en coma flotante. Existen diferentes posibilidades para representar un número en coma
flotante dentro de un programa en C. La más básica consiste en una serie de dígitos (posiblemente precedido con
un signo), que sería la parte entera, un punto decimal y a continuación otra serie de dígitos que constituiría la parte
decimal. Otra alternativa sería utilizar la notación científica mediante la letra e o E. Por ejemplo:
En ningún caso se debe dejar espacios en blanco entre los dígitos y entre los dígitos y el especificador e o E. Esto
NO está permitido:
-74.19 E-23
Como criterio, el compilador asocia las constantes literales anteriores a datos de tipo double. Esto es, se
almacenan en 8 bytes. Esta regla evita los posibles errores de redondeo al realizar operaciones aritméticas, asegu-
rando una mayor precisión en los resultados. Sin embargo, tiene el inconveniente de poder ralentizar la ejecución
de un programa. Es por ello que a veces resulta interesante poder especificar constantes numéricas de 4 bytes. En
este caso, se debe añadir al valor numérico, el sufijo f o F. Por ejemplo,
son constantes numéricas, pero almacenadas en 4 bytes. Resulta ilustrativo observar la salida del siguiente progra-
ma, en el cual se opera con los mismos valores en 8 y 4 bytes, respectivamente
J. Sanguino
40 Capítulo – 3. Introducción al lenguaje C
17
18 return 0;
19
20 }
el resultado en pantalla es
Diferencia: 0.00000762939453125000
Un identificador no puede contener espacios en blanco ni caracteres especiales: ‘ %’, ‘&’, ‘;’ , ‘,’, ‘.’, ‘:’ ,‘+’,
‘-’, etc
El primer carácter de un identificador debe ser obligatoriamente una letra o el símbolo: ‘_’. No puede ser
un dígito.
Se hace distinción entre mayúsculas y minúsculas. De esta manera Velocidad, no es la mismo que
velocidad, ni que VELOCIDAD.
Es recomendable elegir identificadores o nombres de variables de forma que permitan conocer a simple vista,
qué tipo de variable o constante representan, utilizando para ello tantos caracteres como sean necesarios. Esto
simplifica enormemente la tarea de programación, de corrección y mantenimiento de programas. Por tanto, no se
debe rehuir de nombres largos si son ilustrativos de lo que representan.
Código 3.9 – Ejemplo de programa que presenta las direcciones que ocupan las variables en la memoria
1 #include <stdio.h>
2 /* Salida de las direcciones que ocupan las */
3 /* variables en la memoria del ordenador */
4 int main(void)
5 {
6 char carac;
J. Sanguino
3.3 – Funciones básicas de entrada–salida 41
21 return 0;
22 }
A la vista de este código, el símbolo &realDoble por ejemplo, contiene un entero sin signo que representa la
dirección de la variable realDoble.
Cada número representa la dirección del primer byte que ocupa la variable, si el tipo de variable declarado
necesita más de uno. De esta manera, la variable real es del tipo float y utiliza 4 bytes, por tanto la dirección:
2.293.564 corresponde al primer byte de los cuatro reservados. O dicho de otro modo, esta variable ocupa los bytes
que van desde la dirección 2.293.564 a la 2.293.567.
En la función de entrada scanf, que se explica más adelante, se utiliza este operador.
3.3.1. printf()
Imprime en la unidad de salida (el monitor suele ser la salida habitual), el texto y el valor de los datos que se
indiquen. La expresión general de esta función toma la forma:
donde "cadena de control" es un texto junto con una serie de formatos que depende del tipo de variable.
El número de formatos tienen que coincidir con el número de variables y en el mismo orden. En los programas 3.1
y 3.9 se pueden observar diferentes ejemplos de utilización de la función printf(), con distintos formatos. El
tipo de formato depende del tipo de variable. En el cuadro 3.2 se observan los formatos con sus respectivos tipos
de variables asignadas. En el caso de variables enteras del tipo long long, el formato es el mismo que el tipo
int, salvo que se debe añadir doble l antes que el descriptor. Esto es, %lli o %lld. Veamos a continuación un
programa de ejemplo sobre el manejo de formatos
Código 3.10 – Ejemplo de programa de manejo de los formatos con la función printf()
J. Sanguino
42 Capítulo – 3. Introducción al lenguaje C
%c char
%d ó %i int Salida en sistema decimal
%u unsigned Salida en sistema decimal
%x ó %#x Salida en sistema hexadecimal
int ó unsigned
%o ó %#o Salidas en sistema octal
float
%f
double
float
%e Notación exponencial
double
float
%g Ajusta la salida numérica
double
1 #include <stdio.h>
2 /* Salida ejemplo de formatos */
3 int main(void)
4 {
5 char carac = ’r’;
6 int entero = -1234567, i = 2, j = 3;
7 unsigned Ssigno = 396;
8 double realDoble = 345.1234567;
9
10 const double Pepi = 3141592.653589793;
11
12 printf("carac es -> %c <-> %d ASCII\n", carac, carac);
13 printf("entero es -> %d y sin signo -> %u\n", entero, entero);
14 printf("Ssigno es -> %d <-> %x hex <-> %o oct\n", Ssigno, Ssigno, Ssigno);
15 printf("C( %d, %d) = %f\n", i,j,realDoble);
16 printf("realDoble es -> %f <-> %e\n", realDoble, realDoble);
17 printf("Pepi es -> %f <-> %e <-> %g \n", Pepi, Pepi, Pepi);
18 printf("Pepi es -> %.20f <-> %.20e <-> %.20g \n", Pepi, Pepi, Pepi);
19
20 return 0;
21 }
y cuyo resultado es
En la LÍNEA 13 se observa que a una variable entera se la asignado un formato %u (entero sin signo). Hay que
tener cierta precaución porque esto no genera error de compilación, sino que ejecuta la sentencia y presenta un
resultado, que en ocasiones (como el que se muestra) puede ser absurdo o impredecible. Así pues, es importante
que los formatos se ajusten a los tipos declarados de las variables y constantes simbólicas.
J. Sanguino
3.3 – Funciones básicas de entrada–salida 43
En las LÍNEAS 17-18 se utiliza el formato %g que tiene la particularidad de ajustar la salida: puede ser en forma
exponencial o no. Así mismo, tal y como se muestra en la LÍNEA 18, la salida numérica puede ser modificada
variando los formatos (véase figura 3.10),
Indicador de formato
Anchura de campo Dı́gitos decimales
% 18 d % 18.7 f
Tipo de formato
Modificador de posición Modificador de posición
(a) Formato para un número entero (b) Formato para un número real
%: es el indicador de formato.
Anchura de campo: debe indicarse la cantidad de dígitos que ocupará el número. Si es real, hay que incluir
tanto la parte entera como la decimal, incorporando un posible signo negativo. No se contabiliza el punto
decimal. Si no aparece (como en la LÍNEA 18), se ajusta el número de dígitos de la parte entera.
Dígitos decimales: indica el número de dígitos decimales que deben aparecer en la salida. Este es el factor
importante en C. Puede modificar la anchura de campo para mostrar los dígitos decimales establecidos.
Veamos un ejemplo
Código 3.11 – Ejemplo para la variación de formatos en los números reales printf()
1 #include<stdio.h>
2 /* Pruebas con formatos */
3 int main(void)
4 {
5 double Pope = -3141592.653589793;
6
7 printf("/ %d/\n", 12345);
8 printf("/ %2d/\n", 12345);
9 printf("/ %10d/\n", 12345);
10 printf("/ %-10d/\n", 12345);
11 printf("\n\n");
12 printf("/ %8.5f/\n", Pope);
13 printf("/ %-20.5f/\n", Pope);
14 printf("/ %15.5e/\n", Pope);
15 printf("/ %15.12f/\n", Pope);
16
17 return 0;
18 }
cuya salida es
/12345/
/12345/
/ 12345/
/12345 /
J. Sanguino
44 Capítulo – 3. Introducción al lenguaje C
/-3141592.65359/
/-3141592.65359 /
/ -3.14159e+006/
/-3141592.653589793000/
La especificación de anchuras fijas resulta muy útil cuando se desean imprimir columnas de datos (por ejemplo,
los coeficientes de una matriz). Al hacerse el ancho de campo, por defecto, equivalente a la anchura del propio
número, el uso repetido un formato, por ejemplo
Repetición de formatos
printf(" %d %d %d\n", num1, num2, num3);
12 234 1222
4 5 23
22334 2322 10001
Por el contrario se puede conseguir una salida más nítida utilizando campos de anchura fija, lo suficientemente
grandes. Así la sentencia
12 234 1222
4 5 23
22334 2322 10001
Ejercicio:
Como se describe en la tabla 3.2, es posible obtener los valores numéricos enteros positivos en sistemas de numeración
distintos al decimal. Se pide que realices un programa que exprese el número 500 en base decimal, hexadecimal y octal.
N
Secuencias de escape
Como se ha visto en los ejemplos anteriores, también es posible añadir en la cadena de control una serie de
símbolos especiales como ‘\n’. Son las llamadas secuencias de escape. Hay varias, cada una con un cometido
distinto como pueden observarse en el cuadro 3.3. Veamos seguidamente un ejemplo sencillo de utilización de las
secuencias de escape
J. Sanguino
3.3 – Funciones básicas de entrada–salida 45
Secuencia Significado
13 return 0;
14 }
cuya salida es
Esto es lo siguienteCER
Veamos el tabulador
Hola, ya no desaparezco
Esto es lo siguiente
Veamos el tabulador
3.3.2. scanf()
Permite leer la entrada que teclea el usuario. Al igual que la función printf(), emplea una cadena de control
y una lista de argumentos. La mayor diferencia es que es obligado la utilización del operador dirección ‘&’ en los
argumentos. Esto es, se introduce la dirección de la variable, en lugar de su nombre, simplemente. Así pues, se
tiene la siguiente muestra
La función scanf() asigna el valor introducido por el teclado, a la dirección que apunta la variable numero.
Dicho de otro modo, a la variable numero, se el asigna el número tecleado por el usuario. Veamos un ejemplo
J. Sanguino
46 Capítulo – 3. Introducción al lenguaje C
10
11 /* Supongamos que el kilo de oro está a 30.000 euros*/
12
13 valor = peso * 30000.;
14 printf("\n Usted vale %.2f euros\n", valor);
15 printf("\n\n Enhorabuena !!!\n");
16
17 return 0;
18 }
Enhorabuena !!!
Con la función scanf() hay que ser más cuidadoso con los formatos. Por ejemplo, con la función printf() se
podía utilizar el especificador %f tanto para variables de tipo float como double. Sin embargo, aquí ya no, hay
que tener cierta precaución. Veamos el cuál sería el resultado si se cambia el formato %lf, por el especificador %f,
en la función scanf() de la LÍNEA 9. Esto es, si se sustituye la LÍNEA 9 por
Enhorabuena !!!
Presione una tecla para continuar . . .
lo cual es absurdo. Así pues, es importante ajustar la especificación del formato, al tipo declarado para la variable
o constante simbólica.
El lenguaje C declara la función scanf() como entera. Esto quiere decir que devuelve un valor entero
cuando se ejecuta. Este valor entero, corresponde al número de items o argumentos que se leen correctamente. Por
ejemplo, si se tiene una sentencia del tipo
la función scanf(), devolverá el valor entero 2 si la lectura ha sido correcta, es decir, si se han introducido
dos números enteros. Ahora bien, si primero se teclea un número entero y después un carácter (x, por ejemplo),
entonces el valor que devuelve scanf() es 1 , indicando que sólo ha leído un argumento adecuadamente. Por
otra parte, si el orden de la introducción de datos se hubiese realizado al revés (primero el entero y después el
carácter), el valor de retorno hubiese sido 0 , debido a que no lee más datos, a partir de la lectura errónea. Así
pues, scanf() devuelve el número de argumentos leídos correctamente. Esta característica se puede utilizar
comprobar si los datos de entrada se introducen de manera apropiada, en caso contrario, corregir esta introducción
J. Sanguino
3.3 – Funciones básicas de entrada–salida 47
de datos, sin necesidad de interrumpir la ejecución del programa. Como ejemplo se presenta un código, en el que
introducido un número natural, se calcula la suma de todos números que son menores o iguales a él. Este programa
tiene la particularidad de finalizar cuando se introduce el carácter s.
El lenguaje C permite ciertas simplificaciones. Por ejemplo, las LÍNEAS 10-11 pueden simplificarse en una sola,
de la forma
................
y así, eliminar la LÍNEA 20. Esto conlleva poder prescindir de la variable chk. Con lo cual el código quedaría
J. Sanguino
48 Capítulo – 3. Introducción al lenguaje C
Ejercicio:
¿Qué sucede si se elimina la LÍNEA 16 del código anterior? ¿Cuál es el resultado? N
Por otra parte, la función scanf es bastante permisiva y puede proporcionar resultados erróneos si no se tiene
cierta precaución. En el siguiente código se lee una variable de tipo unsigned int, pero si se introduce por
error un número real negativo, la función scanf no detecta ninguna anomalía. En efecto, sea el código
Como se observa, la salida de la función scanf (almacenada en la variable chk) toma el valor 1, lo que indica que
no ha habido error en la lectura del valor de la variable. Pero claramente este valor no era, ni entero, ni sin signo,
como se preveía en la codificación. De hecho era un real negativo. Sin embargo, no se ha detectado dicho error y
J. Sanguino
3.3 – Funciones básicas de entrada–salida 49
además se ha almacenado la parte entera del número: −34. Por este motivo, se necesita tener cierta precaución al
introducir los datos, ya que, como se ha visto, el C suele ser un lenguaje demasiado tolerante.
Otro situación interesante sobre el empleo de la función scanf() se describe a continuación:
Se trata de un programa, que pide un número y lo imprime. A continuación hace lo mismo con un carácter y lo
repite con otro carácter. Sin embargo, en la salida generada no se obtiene lo deseado. En efecto, el resultado, una
vez introducido el número 45 es
Una vez tecleado el número 45 y presionada la tecla ENTER, inmediatamente se ejecuta la siguiente función y
aparece en pantalla
y en este caso, se teclea g. Es decir, únicamente se ha podido introducir un carácter. ¿Qué ha sucedido? Resulta
que el programa, en particular la función scanf(), no lee los datos a medida que se introducen por el teclado,
sino que los lee de un buffer. La palabra buffer (palabra común en el argot de ordenadores) hace referencia a una
zona de memoria que se utiliza para el almacenamiento temporal de datos, generalmente de entrada y salida. Al
pulsar la tecla ENTER, se da vía libre al programa para que capture el carácter o bloque de caracteres que se haya
J. Sanguino
50 Capítulo – 3. Introducción al lenguaje C
introducido en este buffer. También la propia tecla ENTER queda almacenada (o mejor dicho su código ASCII: 10).
Así pues, el buffer suele considerarse una especie de depósito, donde se almacenan los caracteres tecleados, junto
con la ‘tecla ENTER’.
En el programa anterior 3.17, el valor 45 se introduce en la variable var, pero en el buffer queda almacenado
el código de la tecla ENTER. De esta manera, la siguiente función scanf() en la LÍNEA 15, lee el código ASCII
de la tecla ENTER que permanece en el buffer. Al ejecutar la siguiente sentencia en la LÍNEA 16, lo que hace es
imprimir este carácter y por este motivo salta de línea, en la salida. Una vez que el buffer ya está vacío, el siguiente
comando scanf() en la LÍNEA 19, lee correctamente el segundo carácter.
Así mismo, la función scanf() trata de manera diferente los formatos numéricos ( %d, %i, %f, %lf,. . . )
que el formato carácter ( %c). Cuando la función scanf(), tiene un formato numérico, salta todos los espacios
en blanco (incluidos los tabuladores y salto de línea), hasta encontrar un carácter numérico o un signo (+ ó -). Sin
embargo, con el formato carácter, lee todos los espacios en blanco.
Ejercicio:
Realiza un programa en el cual se muestre este comportamiento distinto de la función scanf() con formatos numéricos y
el formato carácter.
N
Se ha introducido la función getchar() después de cada scanf(), con la misión de eliminar la ‘tecla ENTER’
del buffer. Sin embargo, existe una manera de asegurarse de que se limpie completamente el buffer, hasta encontrar
J. Sanguino
3.4 – Operadores 51
la tecla ENTER. Esto se consigue mediante el operador != (distinto), usado en el código 2.2 (pág. 16) y la
instrucción while, empleada en el código 2.3 (pág. 20), con la sentencia
después de cada función scanf(). Únicamente, se obliga a leer caracteres del buffer hasta encontrar la tecla
ENTER , que indicará que se ha terminado de introducir datos.
Por otra parte, existe una función que toma un carácter de programa y lo envía a la pantalla. Se denomina
putchar(). En este caso, sí tiene argumento, que es el carácter que debe imprimirse en pantalla. Tienen sentido
las siguientes sentencias:
Ejercicio:
Utilizando las funciones getchar(), putchar() y la instrucción while, se pide que realices un programa que lea
un texto del teclado y lo imprima en la pantalla y además que cuente el número de caracteres introducido7 .
N
3.4. Operadores
Los operadores constituyen órdenes que pueden actuar sobre una variable (operadores unarios) o sobre dos
variables (operadores binarios). En C hay diversos tipos de operadores: aritméticos, lógicos, relacionales, incre-
mentales, asignación e incluso, de bits. Unos se utilizan más que otros, en esta sección se tratarán de los más
básicos, que permitan abordar los aspectos fundamentales de la programación en el lenguaje C. No obstante, para
obtener una información más detallada sobre los operadores, se puede consultar el libro de Rodríguez–Losada et
al [19, pág. 23].
Ejemplo de asignación
xyz = 2345;
introduce el valor 2345 en la variable ‘xyz’. Por otra parte, la orden (como ya se ha planteado)
Ejemplo de asignación
i = i + 1;
siendo muy común en programación, desde el punto de vista matemático, carece de sentido. Quiere decir: encuentra
el valor de la variable i; a tal valor, súmale 1 y a continuación asigna este nuevo valor, a la variable de nombre i.
No tienen sentido en programación, por tanto, sentencias del tipo:
7
Una orientación sobre la solución puede encontrarse en el código 4.10 (pág. 82). No obstante, el objetivo es que el alumno trabaje este
ejercicio, antes de consultar la solución.
J. Sanguino
52 Capítulo – 3. Introducción al lenguaje C
Símbolo Significado
+ Suma
- Resta
* Producto
/ División
% Módulo (o resto de la división entera)
Se trata de operadores binarios que aceptan dos operandos de tipo numérico y devuelve un valor del mismo
tipo que el de los operados. En el cuadro 3.4 se muestran los operadores aritméticos admitidos en C. Veamos un
ejemplo:
y cuyo resultado es
J. Sanguino
3.4 – Operadores 53
En la LÍNEA 13 se produce un proceso de promoción: la variable caracter pasa tipo entero para multiplicarlo
por 2; después el resultado se promociona a float para sumar la variable real y por último se transforma en
entero al almacenarse en la variable entero, pudiéndose producir una pérdida de rango. Sin embargo, en la LÍNEA
14 el proceso es una promoción: la variable caracter se convierte en punto flotante para multiplicarla por 2.0;
a continuación la variable entero también se pasa a float para sumarle el resultado anterior y por último se
almacena en la variable real.
Es posible realizar una conversión de tipos de manera explícita con el operador cast. Consiste en poner entre
paréntesis el tipo al cual quiere transformarse la variable que queda a su derecha. Por ejemplo, la operación
J. Sanguino
54 Capítulo – 3. Introducción al lenguaje C
realiza una división entera, si entero es una variable de tipo int. Esto es, si entero no es un múltiplo de 28
entonces únicamente se almacena la parte entera de la división en numreal, aunque está sea de tipo decimal (es
decir, float o double). Si se quiere que la división sea real, esto es, que almacene la parte decimal, implica
que además de que numreal sea decimal, la división también lo deberá ser. Para conseguir esto, se promociona
explícitamente a la variable entero a decimal, con el operador cast, como indica la operación
9
10 ent1 = real1; // conversión a entero, implícita
11 printf (" %f se transforma en el entero %i\n\n", real1, ent1);
12
13 real1 = ent2; // conversión a real, implícita
14 printf (" %i se transforma en el real %f\n\n", ent2, real1);
15
16 real1 = ent2 / 100; // división entera
17 printf (" %i dividido por 100 es %f\n\n", ent2, real1);
18
19 real2 = ent2 / 100.0; // división real
20 // (entero dividido por real). Conversión implícita
21 printf (" %i dividido por 100.0 es %f\n\n", ent2, real2);
22
23 real2 = (float) ent2 / 100; // conversión explícita
24 printf ("(float) %i dividido por 100 es %f\n\n", ent2, real2);
25
26 return 0;
27 }
y cuya salida es
J. Sanguino
3.4 – Operadores 55
X *= 3*Y + 12;
que es equivalente a
X = X * (3*Y + 12);
7
8 incre1 = 2 * ++ent;
9 ent = 4;
10 incre2 = 2 * ent++;
11 printf("incre1 = %5d, incre2 = %5d\n", incre1, incre2);
12 printf("ent -> %d\n", ent);
13
14
15 return 0;
16 }
J. Sanguino
56 Capítulo – 3. Introducción al lenguaje C
la salida en pantalla es
El proceso es el siguiente. En la LÍNEA 8 el operador está en la modalidad prefijo. Esto implica que la variable
ent aumenta en una unidad, antes de operar. Con lo cual ent toma el valor 5. A continuación opera y el valor
almacenado en incre1 es 10, como se observa en la salida. Sin embargo, en la LÍNEA 10, se tiene la modalidad
sufijo. Así pues, se opera con el valor ent asignado. Es decir, ent = 4 y a continuación se opera. Con lo cual
la variable incre2 almacena el valor 8, como se muestra en la salida. Una vez terminadas las operaciones, se
incrementa en una unidad el valor de ent, cuyo resultado es 5.
Una consecuencia importante de esta manera de proceder, es que estos operadores tiene una elevada preceden-
cia (el operador ++ o -- únicamente afecta a la variable que acompaña); tan sólo superados por el paréntesis. Por
ello
X * Y++;
significa
(X) * (Y++);
y NO
(X * Y)++;
Ahora bien, la precedencia no hay que confundirla con el orden de evaluación de los operadores. La precedencia
establece la relación entre el operador y la variable sobre la que actúa. En el ejemplo del código 3.22, se ha visto
en la LÍNEA 10, operaciones con la variable ent y sólo después de realizarlas, incrementaba la variable en una
unidad. Por tanto, cuando nos encontremos un i++ (respectivamente, i--) como parte de una expresión, se puede
interpretar como: utiliza i; a continuación increméntalo (respectivamente, decreméntalo). Por el contrario, ++i
(respectivamente, --i) significaría: incrementa (respectivamente, decrementa) i; a continuación, utilízalo.
Una fuente de problemas en el empleo de este tipo de operadores son sentencias de la forma:
en este caso, no está claro que el compilador vaya ejecutarla en el orden en que pensamos: empieza por entero/2
y sigue con la línea. Pues bien, puede ocurrir que calcule el último término antes, realice el incremento y use el
nuevo valor de entero para evaluar entero/2. Para evitar este tipo de problemas se recomienda
1. No se debe utilizar operadores incremento o decremento en variables que se emplean más de una vez como
argumento de una función:
2. No se debe utilizar operadores incremento o decremento en variables que se empleen más de una vez en una
misma expresión.
J. Sanguino
3.4 – Operadores 57
Operador Significado
verdadero = 1; falso = 0
En la LÍNEA 7 comprueba que es verdadero que 16 > 8 y este resultado lo almacena en la variable: verdadero.
En este caso el valor es 1. Sin embargo, en la LÍNEA 8 comprueba si son iguales 15 y 3; al ser distintos el resultado
es falso y almacena en la variable falso, el valor 0. Por tanto, 0 es falso y 1 es verdadero. Realmente para C,
todo valor numérico (generalmente entero) que no tome el valor 0 es verdadero (con la sentencia if-else, ya
vista en el código 2.2 de pág. 16, se puede probar fácilmente este hecho).
Algunas veces es útil comparar dos o más expresiones de relación. Para ello se emplean los operadores lógicos.
En C existen tres operadores lógicos, como muestra el cuadro 3.7.
J. Sanguino
58 Capítulo – 3. Introducción al lenguaje C
únicamente verdadero, si
&& y exp1 && exp2
exp1 y exp2 son ciertas
únicamente falso, si
|| o exp1 || exp2
exp1 y exp2 son falsas
16 return 0;
17 }
obteniéndose en pantalla
En el código 3.24, la LÍNEA 7 es falsa porque sólo una de las desigualdades es cierta. Sin embargo, la expresión de
la LÍNEA 10 es verdadera porque sólo una de las desigualdades es cierta. La sentencia de la LÍNEA 13 es verdadera
ya que 3 no es mayor que 8. Todos estos resultados se pueden observar en la salida ya que, no olvidemos que para
C, es falso si es = 0, pero verdadero si es 6= 0.
Ejercicios:
1.– Modifica el código 3.15 (pág. 47) para parar la ejecución del programa en el momento que se introduzca un valor
negativo.
2.– Codifica un programa que lea un número del teclado y determine si pertenece al intervalo (−1, 32].
N
J. Sanguino
3.4 – Operadores 59
() paréntesis
! ; ++ ; -- no lógico y los incrementales
* ; / ; % multiplicación y división
+ ; - suma y resta
< ; <= ; > ; >= desigualdades
== ; != igual que; distinto que
&& y lógico
|| o lógico
= asignación
Tabla 3.8 – Precedencia de los operadores. De mayor (más arriba) a menor (abajo)
que ||, estando ambos situados por debajo de los operadores de relación y por encima de la asignación. Por
consiguiente la expresión
se interpreta como
Orden de evaluación
Cuando se estudiaron los operadores incrementales y decrementales (véase subsección 3.4.4 pág. 55), ya se
comentó sobre el orden de evaluación de los operadores no tenía que coincidir con su precedencia. Ahora bien, es
importante tener en cuenta que normalmente en C, no se garantiza qué parte de una expresión compleja se evalúa
primero. Por ejemplo, en la sentencia
la expresión 3 + 5 puede evaluarse antes que 15 − 9 o al revés, sin embargo la precedencia garantiza que ambas
se realizarán antes que efectuar el producto. Esto es una ambigüedad que se dejó a propósito en el lenguaje, a
fin de permitir a los diseñadores de los compiladores, pudiesen preparar versiones más eficientes. Sin embargo,
existe una excepción en el tratamiento de los operadores lógicos. En C se garantiza que las expresión lógicas se
evalúan de izquierda a derecha. También queda garantizado que tan pronto se encuentre un elemento que invalida
la expresión completa, cesa la evaluación de la misma. Por ejemplo
indica que si la variable numero tiene un valor igual a 0, entonces la expresión es falsa y el resto NO se evalúa.
En la tabla 3.8 se muestra la precedencia o prioridad de los operadores, que no tiene que coincidir con el orden de
evaluación en una expresión, como ya se ha comentado.
J. Sanguino
60 Capítulo – 3. Introducción al lenguaje C
3.5. Preprocesador
Se trata de un componente característico del lenguaje C, que no es habitual en otros lenguajes de programa-
ción. El preprocesador actúa sobre el código antes de que empiece la compilación, propiamente dicha, para realizar
ciertos cometidos. Uno de estos cometidos consiste, por ejemplo, en incluir las funciones básicas de entrada y sa-
lida de datos mediante la instrucción #include, como se ha visto en los ejemplos de programas anteriores.
Existen diferentes tipos de comandos propios de preprocesador, también llamados directivas. Todos ellos
deben empezar por el símbolo #. En esta sección únicamente se tratarán las directivas: #define e #include.
3.5.1. #define
Ya se ha visto en la sección 3.2 (pág. 27) que una manera de definir constantes simbólicas es mediante el
cualificador const. Esto se puede hacer también en C utilizando la directiva #define en el preprocesador.
Incluso con esta directiva es posible definir funciones sencillas con argumentos, como muestra el siguiente ejemplo
18 PA(area);
19 PL(longitud);
20
21
22 return 0;
23 }
En la LÍNEA 4 se establece PI como una constante simbólica, que posteriormente se utiliza en las LÍNEAS 15-16.
Cualquier sentencia dentro de la función main del tipo:
PI = 45.7;
hubiese dado un error de compilación. Por tanto, este procedimiento es equivalente a utilizar el cualificador
const8 . Así mismo, en las LÍNEAS 5-6 se utiliza, de nuevo la directiva #define, para definir funciones con
un argumento (pueden ser más, si es necesario) que después se emplean en las LÍNEAS 18-19.
8
No obstante, cada vez se utiliza menos este procedimiento y se tiende a emplear más el cualificador const, por la compatibilidad con
C++
J. Sanguino
3.6 – Ficheros 61
Como se observa en el código 3.25, cada utilización de la directiva #define consta de tres partes. En primer
lugar, la directiva #define. La segunda parte, consiste en establecer la palabra que se quiere definir, que suele
denominarse ‘macro’ dentro del mundillo de la programación. La macro debe ser una única palabra, cuyo nombre
debe seguir las mismas reglas que se utilizan para denominar las variables en C. En la tercera parte se escriben una
serie de caracteres que van a ser representados por la macro.
Generalmente las macro se utilizan para definir funciones sencillas o establecer constantes simbólicas, como
se ha visto. En caso de necesitar definir funciones más complejas se emplean procedimientos que se estudiarán
más adelante.
3.5.2. #include
Cuando el preprocesador encuentra un comando o directiva #include busca el fichero que atiende por el
nombre está situado a continuación y lo incluye en el fichero actual. El nombre del fichero puede venir de dos
formas:
#include <stdio.h>
#include "misfunciones.h"
En el primer caso, los paréntesis angulares le está diciendo que busque el fichero en un directorio estándar del
sistema. En el segundo caso, las comillas le dicen que lo busque en el directorio actual (en donde tiene el fichero
fuente) en primer lugar, y si no lo encuentra, que lo busque en el directorio estándar.
La cuestión de ¿por qué es necesario incluir ficheros? se contesta simplemente: porque tienen la información
que se necesita. El fichero stdio.h, por ejemplo, contiene generalmente las definiciones de printf, scanf,
EOF (End Of File) y otras más.
El sufijo .h se suele emplear para ficheros de encabezamiento (header), es decir, con información que debe ir
al principio del programa. Los ficheros cabecera (como también se llaman) consisten, generalmente, en sentencias
para el preprocesador. Algunos como stdio.h vienen con el sistema, pero el usuario puede crear los que quiera
para dar su toque personal a los programas.
3.6. Ficheros
En ciertas ocasiones, debido a la cantidad de datos obtenidos en la salida de un programa, resulta imposible
realizar un análisis sobre la pantalla. Para solventar esta situación, se dispone de los ficheros, que permiten alma-
cenar los datos para un estudio posterior. Así pues, una posible definición de fichero sería: una porción de memoria
permanente que puede consultarse cuando el usuario lo requiera. Desde el punto de vista del C, el concepto de
fichero es más complicado, pero esto algo que no se estudiará en este documento. Sin embargo, resulta importante
tener claro el procedimiento que debe realizarse para escribir o leer datos de un fichero mediante el lenguaje C:
1. Primeramente hay que abrir el fichero. Para ello es necesario introducir el nombre del fichero. El sistema
comprueba si el fichero con ese nombre existe. Si no, y según en qué casos, lo puede crear.
2. Una vez abierto, hay que especificar qué procedimiento de E/S (E NTRADA /S ALIDA) se va a realizar con dicho
fichero. Si se utilizará para leer, habrá que especificar que debe abrirse en modo lectura (read mode). Si se
utilizará para escribir datos, entonces se abrirá en modo escritura (write mode) y por último si se abre para
añadir datos se deberá abrir en modo adición (append mode). En los dos últimos casos: modo escritura y
modo adición, en caso de no existir los ficheros, el C los crea. En el caso del modo lectura, si no existe el
fichero, el sistema manda una señal de error.
3. Por último hay que informar al programa cómo puede identificar el fichero. Para ello, una vez abierto el
fichero, C asocia a dicho fichero un número. Dicho número corresponde al valor de una variable del tipo
puntero a fichero9 , que hay que declarar antes de abrir el fichero. Mediante este número, el programa
ya puede identificar al fichero y con las funciones adecuadas del lenguaje C, ya se pueden realizar las
9
El concepto de puntero se tratará más adelante en el documento (véase capítulo 6, pág. 143) de manera más amplia.
J. Sanguino
62 Capítulo – 3. Introducción al lenguaje C
operaciones de E/S en este fichero. Es frecuente llamar a la variable canal de interconexión o simplemente
canal.
La función fopen permite abrir un fichero, adjudicar un modo (lectura, escritura o adición) y devolver un valor
para el puntero a fichero que identificará el programa con el fichero. La función tiene dos argumentos:
El primer argumento: cadena de caracteres, hace referencia al nombre del fichero que se va a abrir. Si es
una constante, esto es, un nombre, debe escribirse entre comillas: "Datos.dat". El segundo: modo, describe el
uso que se le va a dar al fichero, identificado por tres letras:
"r": un fichero de lectura (read). Únicamente se puede leer datos. Si el fichero no existe, devuelve un valor
que se asocia al nombre de NULL que indica un fallo en la apertura. Un ejemplo de utilización del valor de
puntero a fichero NULL, puede apreciarse en el siguiente trozo de código:
#include <stdio.h>
int main(void)
{
FILE *fi;
fi = fopen("Datos.dat", "r");
if (fi == NULL){
printf("¡¡¡¡ ERROR en la apertura del fichero: Datos.dat !!!!")
;
return 5; //
} else {
// LECTURA DE DATOS DEL FICHERO
.............................
.............................
"w": un fichero de escritura (write). Se utiliza para escribir datos. Si no existe el fichero en el directorio, lo
crea. Si existe, borra todo lo que hay en su interior e introduce los nuevos datos generados por el programa.
"a": un añadido al fichero (append). Si el fichero existe, entonces añade los nuevos datos generados, a
partir de los ya existentes en el fichero.
Por último, si no ha habido ningún error en la apertura, la función fopen retorna un valor que se almacena en
un puntero a fichero (canal), que será el valor que utiliza el programa para conectarse con el fichero. En el caso
anterior, se almacena en la variable fi.
Un ejemplo de utilización de la función fopen puede verse en el código 3.26. En la LÍNEA 10, se abre un
fichero llamado Datos.dat en modo escritura ("w"). Si no está en el escritorio, lo crea y si existiese, borra todo
su contenido. Así mismo, la función fopen retorna un valor, que se almacena en la variable puntero a fichero: fi.
Es importante destacar que esta variable ha sido declarada previamente, en la LÍNEA 5.
Así mismo, existe la función fclose, cuya misión es cerrar el fichero. Esto es, desconectar el fichero del
programa. Habitualmente si no se pone explícitamente, el sistema provoca el cierre automáticamente, al terminar
la ejecución. No obstante, es una buena costumbre de programación hacer esta desconexión explícitamente. La
función fclose sólo tiene un argumento, que es la variable puntero a fichero obtenida con la función fopen. En
la LÍNEA 18 del código 3.26, puede observarse un ejemplo de utilización de fclose.
Uno de los empleos más habituales de los ficheros es almacenar datos. Estos datos se pueden escribir en el
fichero mediante el comando fprintf (véase el código 3.26). Es similar a la orden printf, ya estudiada. La
única diferencia está en que fprintf, necesita como primer argumento el valor del puntero a fichero, obtenido
mediante la función fopen.
J. Sanguino
3.7 – Funciones matemáticas 63
También es posible leer datos con los comandos de lectura, por ejemplo, fscanf, que tiene las mismas
particularidades que el comando fprintf. No obstante, en este documento nos centraremos, fundamentalmente
en la escritura de datos en los ficheros. Para una información más extensa, puede consultarse el libro de Rodriguez–
Losada et al[19].
7 FILE *fi;
8
9 fi = fopen("Datos.dat", "w");
10
11 incre1 = 2 * ++ent;
12 ent = 4;
13 incre2 = 2 * ent++;
14 fprintf(fi, "incre1 = %5d, incre2 = %5d\n", incre1, incre2);
15 fprintf(fi, "ent -> %d\n", ent);
16
17 fclose(fi);
18
19 return 0;
20
21 }
La salida es idéntica a la obtenida mediante el código 3.22 (pág. 55), pero con la diferencia que con este programa,
la salida se almacena en el fichero Datos.dat que se crea en el directorio en el que estemos.
El C proporciona una señal para indicar que se ha terminado de leer los datos de un fichero. Se encuentra
definida en el fichero stdio.h y se representa por EOF.
J. Sanguino
64 Capítulo – 3. Introducción al lenguaje C
J. Sanguino
3.8 – Problemas 65
3.8. Problemas
1. Realiza un programa que muestre, para cada tipo definido en el capítulo, el número de bytes que tiene
asignado, junto con el rango y la precisión. Por ejemplo, parte de la salida podría ser:
2. Escribe un programa que dado un carácter, obtenga su correspondiente código ASCII en decimal, octal y
hexadecimal. Un ejemplo de ejecución del código podría ser:
Seguidamente, haz otro programa que ejecute el proceso inverso esto es, que se pueda introducir un valor
entre 0 y 255 y obtenga el correspondiente carácter. Analiza si existe alguna diferencia entre utilizar char
y unsigned char.
3. Realiza un programa que determine que para C, cualquier valor distinto de 0 es verdadero, pero si es 0,
entonces es falso10 .
9
F = C + 32
5
Se pide que hagas un programa que pida grados centígrados y los transforme en grados Fahrenheit. Haz
otro programa que realice el proceso inverso.
Peso en Kg.
IMC =
Altura en metros2
da una estimación sobre el peso correcto según la altura. Se pide que realices un programa en que dados un
peso y una altura, se calcule el IMC. Una posible ejecución podría ser:
10
Una posible solución puede encontrarse en el código 4.1 (pág. 72). No obstante, el objetivo es que el alumno trabaje este ejercicio,
antes de consultar la solución.
J. Sanguino
66 Capítulo – 3. Introducción al lenguaje C
6. Escribe un programa en el que se introduzcan las coordenadas cartesianas de un punto del plano y devuelva
por pantalla su distancia al origen.
(Nota: se pueden utilizar las funciones matemáticas del cuadro 3.9, pág. 64).
7. Escribe un programa en el que se introduzcan las coordenadas de un punto y devuelva sus coordenadas
polares.
(Nota: se pueden utilizar las funciones matemáticas del cuadro 3.9, pág. 64).
9. Haz un programa que lea un entero sin signo y lo imprima, pero debe comprobar que la entrada es correcta.
Esto es, si se introduce un carácter, ha de tenerse la posibilidad de repetir la entrada, sin detener la ejecución
del programa. Un posible ejemplo sería:
10. Siguiendo con la idea del problema anterior, haz un programa que lea dos números decimales y los imprima
(por supuesto, comprobando que la entrada es correcta). A continuación que el programa lea dos caracteres
(con comprobación) y que los imprima. Por último, que lea un entero (también con comprobación) y que lo
imprima.
J. Sanguino
3.8 – Problemas 67
11. Sabiendo el tamaño de bytes de memoria que reserva un tipo unsigned long long, haz un programa
que realice una tabla con los factoriales del 1 al 25 y se almacene una una variable de este tipo. El resultado
debe volcarse en un fichero llamado Datos.dat. ¿Qué observas del resultado?. Repite el mismo programa
pero almacenando los factoriales en una variable decimal double. Compara los resultados, ¿puedes sacar
alguna conclusión?
se pide que realices un programa que determine el mayor número natural n0 tal que se cumpla:
n
1
x=1+ >1
2
¿Obtienes el mismo resultado operando con tipos float que operando con tipos double?
J. Sanguino
CAPÍTULO 4
4.1. Introducción
Lo que hace distinto un ordenador de una potente calculadora (no programable), es que el ordenador puede
soportar programas que sean capaces de tomar decisiones o repetir una secuencia de órdenes. Esto se realiza en
los lenguajes de programación mediante las sentencias de control, que deben presentar tres características básicas:
Repetición de una secuencia de instrucciones hasta que se cumpla una determinada condición.
En este capítulo, se van a tratar las sentencias de control más habituales en el lenguaje C. Se comenzará por
aquellas sentencias de control de flujo que suelen ser más frecuentes, para continuar con aquellas que permiten
elaborar programas más complejos.
si (condición) entonces
Sentencias (V)
fin-si
si (condición) entonces
Sentencias (V)
en_otro_caso
Sentencias (F)
fin-si-en_otro_caso
69
70 Capítulo – 4. Control del flujo
si (condición) entonces
Sentencias (V)
en_otro_caso-si (condición) entonces
Sentencias (V)
en_otro_caso-si (condición) entonces
Sentencias (V)
...
...
en_otro_caso
Sentencias (F)
fin-si-en_otro_caso-si
Bloque: según_sea. Se trata de otra sentencia de selección múltiple, más adecuada cuando hay un número
elevado de opciones. Su descripción sería
4.2.1. Sentencia if
Se trata de una sentencia de tipo condicional. Existen diferentes modelos, dependiendo de la complejidad de
las opciones de la decisión, que se planteen en el programa.
if Es la opción más sencilla. Ya se introdujo en la sección 2.4, en el código 2.2 (pág. 16). Permite ejecutar una
instrucción simple o un conjunto de ellas, según se cumpla o no una determinada condición. Esta sentencia
tiene la siguiente expresión general
if(expresion) sentencia;
o bien
if(expresion) {
sentencia;
sentencia;
.
. /* <--- BLOQUE IF */
.
J. Sanguino
4.2 – Sentencias de selección 71
sentencia;
}
if...else Esta sentencia permite realizar una bifurcación, ejecutando una parte u otra según se cumpla o no
cierta condición. Este tipo de condición ya se utilizó en el código 2.2 (pág. 16) y en el código 2.3 (pág. 20)
La expresión toma la forma:
if (condición) {
sentencia;
.
. /* <--- BLOQUE IF */
.
sentencia;
} else {
sentencia;
.
. /* <--- BLOQUE ELSE */
.
sentencia;
}
if (condición_1){
sentencia;
.
. /* <--- BLOQUE IF */
.
sentencia;
} else if (condición_2) {
sentencia;
.
. /* <--- BLOQUE ELSE IF */
.
sentencia;
} else if (condición_3) {
sentencia;
.
. /* <--- BLOQUE ELSE IF */
.
sentencia;
} else {
sentencia;
.
. /* <--- BLOQUE ELSE */
.
sentencia;
}
J. Sanguino
72 Capítulo – 4. Control del flujo
4.2.2. Ejemplos
Cuando se estudiaron los operadores de relación (véase subsección 3.4.5, pág. 56) ya se comentó que para el
lenguaje C todo lo que no sea cero es verdad. En efecto, esto se puede justificar a partir del siguiente código
con el resultado de
Este ejemplo se puede repetir para cualesquiera números distintos de cero, y el resultado será el mismo. Por tanto,
cualquier valor distinto de cero es verdad para C. En el siguiente ejemplo se codifica en C, la función
−x si x < 0
f (x) = x2 si 0 6 x < 1
sen(π · x) si 1 6 x
J. Sanguino
4.2 – Sentencias de selección 73
El programa sería:
En este caso, el argumento de la función se introduce por pantalla y según su valor, se distribuye por los distintos
bloques. Debido al funcionamiento del if múltiple, no hace falta especificar la condición 0 6 x < 1, sino
simplemente x < 1, ya que si es menor que 0, ejecuta el BLOQUE 1. En el BLOQUE 3 se utiliza la función seno tal
y como la identifica el C. Para obtener una descripción más completa de las funciones matemáticas que soporta el
lenguaje C, se recomienda consultar el libro de Kernighan y Ritchie [10]. En la variable resultado, se almacena
el valor de la función que se escribe en un fichero con el nombre Datos.dat, junto con el valor introducido.
if (condición)
sentencia1;
else
sentencia2;
J. Sanguino
74 Capítulo – 4. Control del flujo
Para los valores introducidos por el teclado de: 3.467, −247.23 y −9 respectivamente, el resultado escrito en el
fichero Datos.dat es
Este ejemplo tiene la particularidad de utilizar el operador condicional para definir dos funciones de maneras
distintas. La primera, para la función ABS, se establece mediante una macro por medio de la directiva #define,
para que sea manipulada directamente por el preprocesador. La segunda forma, se plantea en la LÍNEA 19 para
definir la función max.
switch (expresión){
case cte_1 :
sentencia;
.
. /* <--- BLOQUE SWITCH 1*/
.
sentencia;
case cte_2 :
sentencia;
.
. /* <--- BLOQUE SWITCH 2*/
J. Sanguino
4.2 – Sentencias de selección 75
.
sentencia;
..................
..................
case cte_n :
sentencia;
.
. /* <--- BLOQUE SWITCH n */
.
sentencia;
default :
sentencia;
.
. /* <--- BLOQUE DEFAULT */
.
sentencia;
}
J. Sanguino
76 Capítulo – 4. Control del flujo
Es la vocal: a
Es la vocal: e
Es la vocal: i
Es la vocal: o
Es la vocal: u
Es una consonante. No me mientas
que no es lo que uno esperaría. Hagamos otra prueba: se teclea la letra i y se obtiene
Es la vocal: i
Es la vocal: o
Es la vocal: u
Es una consonante. No me mientas
y si se teclea f el resultado es
Todas estas salidas coinciden con la definición dada para la sentencia switch, pero es claro que no se ajusta
a lo que desearía de un programa que te devuelve la vocal introducida. Para evitar esta situación se introduce la
sentencia break. Esta proposición, fuerza la salida de la sentencia switch y el programa pasa a ejecutar la
sentencia a continuación de la llave } del switch. Con la introducción de break en cada BLOQUE SWITCH,
ya el programa se comporta como era de esperar. Esto es, el programa quedaría de la forma:
J. Sanguino
4.3 – Sentencias de repetición 77
25 case ’u’ :
26 printf("Es la vocal: u\n");
27 break;
28 default :
29 printf("Es una consonante. ");
30 printf("No me mientas\n \n");
31 } /* fin switch */
32 else
33 printf("Sólo letras minúsculas\n\n");
34 printf("Introduce otra vocal (minúscula) o @ para terminar -> ");
35 } /* end if nueva línea*/
36 } /* end while */
37
38 return 0;
39 }
Es la vocal: a
Se teclea la i y se obtiene
Es la vocal: i
que es lo lógico.
La sentencia break también proporciona una salida anticipada de un for, while y do-while, tal y
como lo hace en el switch.
J. Sanguino
78 Capítulo – 4. Control del flujo
Bloque: repetir-hasta_que. Es un bloque de repetición pero en este caso, pregunta si debe seguir con la
repetición del bucle, después de ejecutar las instrucciones por primera vez. Esto es, a diferencia del modelo
mientras, en este caso, primero se ejecuta y después evalúa la condición para seguir ejecutando el bucle.
repetir:
Sentencias
hasta_que (condición)
Seguidamente se analizan con detalle las instrucciones del lenguaje C asociadas a cada una de estas estructuras
de repetición.
while (condición)
sentencia;
while (condición){
sentencia;
.
. /* <--- BLOQUE WHILE */
.
sentencia;
}
J. Sanguino
4.3 – Sentencias de repetición 79
1 #include <stdio.h>
2 /* Ejemplo del bucle while */
3
4 #define TOPE 100
5
6 int main(void)
7 {
8 int suma = 0, numero;
9 int suma_Gauss = 0;
10
11 FILE *fi;
12
13 fi = fopen("Datos.dat", "w");
14
15 numero = 1;
16
17 while (numero <= TOPE){
18 suma += numero; /* suma = suma + numero */
19 numero++;
20 }
21
22 fprintf(fi, "La suma de los %d ", TOPE);
23 fprintf(fi, " primeros números es %d\n\n", suma);
24 fprintf(fi,"Veamos si me he equivocado: ");
25 fprintf(fi,"voy a aplicar la regla de Gauss");
26
27 suma_Gauss = TOPE * (TOPE + 1) / 2;
28 fprintf(fi, "\n \n suma_Gauss es %d", suma_Gauss);
29 if (suma == suma_Gauss)
30 fprintf(fi, "\t Bien !!!! sé sumar");
31
32 else
33 fprintf(fi, "\t Vaya !!!! fallé, tengo que repasar");
34
35 fclose(fi);
36
37 return 0;
38 }
Es habitual que a los programadores de C les guste compactar el código (es lógico, pues es un lenguaje creado con
esta intención) y por tanto, es frecuente reescribir el bucle de la siguiente manera:
es decir, añadir el operador incremento a la condición. Sin embargo, con este pequeño cambio el resultado
sería el siguiente:
J. Sanguino
80 Capítulo – 4. Control del flujo
¿Qué es lo que falla? En la LÍNEA 15 del código 4.6 se realiza la inicialización de la condición del bucle while,
con la asignación del valor 1 a la variable numero. La variable numero toma entonces el valor 1, pero con el
operador incremento aumenta en una unidad, después de comparar con el valor de TOPE. Con lo cual, entra con
el valor de 1, se compara con TOPE, una vez hecha esta comparación, se incrementa en una unidad, en este caso 2
y con este valor entra en el bucle. Así pues, se calcula la suma desde 2 al 101, obteniéndose 5150. Para tratar de
remediar la situación, se sustituye la LÍNEA 15 por
entero = 0;
Ahora la razón está en que el operador incremental tiene una expresión sufija. Esto quiere decir, como ya se ha
comentado, que primero compara con el valor de TOPE y después realiza el incremento. Al comienzo, el proceso
es correcto, pero cuando llega numero al valor TOPE, entonces comprueba que son iguales los valores y da paso
a ejecutar el bucle. De manera inmediata aumenta en una unidad el valor numero, con lo cual entra en el bucle
con el valor de TOPE + 1. Es decir, se está calculado, realmente la suma desde 1 hasta 101, esto es, 5151. El
siguiente paso sería intercambiar la expresión sufija por la prefija, en el operador incremento, pero manteniendo el
valor inicial de entero a cero:
con este procedimiento, se hace primero la comparación de la variable numero con el valor de TOPE y después
se realiza el incremento de la variable numero. Así pues, la salida ya es correcta
A la vista de estos ejemplos, hay que tener cierta precaución al utilizar los operadores incrementales y/o decre-
mentales junto con el bucle while. Puesto que esta situación suele ser fuente de numerosos errores no detectados
por el compilador, tal vez merece la pena, insistir un poco más en ello. Sean los ejemplos
J. Sanguino
4.3 – Sentencias de repetición 81
14 }
cuya salida es
Una vez más, se observa que el procedimiento sufijo del código 4.8 obliga a que se realice una vez más el bucle,
que con el operador en forma prefija del código 4.7. En el programa 4.8, incrementa la variable entero después
de comprobar si es menor que 3, mientras que el código 4.7, lo incrementa antes de comprobar si es menor que 3.
Así pues, los operadores incrementales y decrementales resultan muy prácticos a la hora de programar, pero hay
que utilizarlos con cierta cautela porque pueden producir serios quebraderos de cabeza.
Otro aspecto en el cual hay que tener cierta precaución es la posición del punto y coma (;). Es un aspecto de
su sintaxis que puede llevar a ciertos errores que a veces, son difíciles de localizar. En efecto, sea el programa
num es 4
J. Sanguino
82 Capítulo – 4. Control del flujo
El problema surge porque en la LÍNEA 7 se ha situado el punto y coma ; inmediatamente después de la sentencia
while. Por tanto, la LINEA 8 ya no se considera parte de la instrucción while.
Debido a la estructura del bucle while es posible operar fácilmente con caracteres utilizando las funciones
getchar() y putchar() (véase subsección 3.3.3 en la pág. 50). En el siguiente ejemplo, el usuario escribe
una frase y el programa reproduce la frase y cuenta el número de caracteres:
15 return 0;
16 }
Una vez que el usuario escribe una frase en la pantalla y teclea ENTER, los caracteres almacenados en el buffer
de entrada se procesan y se obtiene una copia de la frase, en la pantalla. La ‘tecla ENTER’ está almacenada en la
constante PARA, definida mediante la directiva #define. Para la frase
Me gusta aprender C
Me gusta aprender C
Me gusta aprender C
El código 4.10 puede compactarse (como tanto les gusta a los programadores de C) sustituyendo las LÍNEAS
9–12 por una expresión más profesional
Para evitar que la frase aparezca dos veces en la pantalla, se puede redireccionar la segunda frase a un fichero.
Esto se consigue con la función putc(). Es similar a la función putchar() pero ahora hay que añadir, como se-
gundo argumento, el puntero al fichero. El programa anterior, pero con la salida redirigida al fichero Datos.dat,
sería:
Código 4.11 – Bucle while y funciones getchar() y putchar() con salida redirigida a un fichero
1 #include <stdio.h>
2 /* Ejemplo para el bucle while y salida a fichero */
3 #define PARA ’\n’
4 int main(void)
J. Sanguino
4.3 – Sentencias de repetición 83
5 {
6 int contador = 0;
7 FILE *fi;
8
9 fi = fopen("Datos.dat", "w");
10
11 printf("Teclea una frase \n\n");
12 while (putc(getchar(), fi) != PARA)
13 contador++;
14 fprintf(fi,"\n He leído un total de %d caracteres. \n", contador);
15
16
17 fclose(fi);
18 return 0;
19 }
En la LÍNEA 12 se observa la utilización de la función putc(). La salida obtenida en el fichero Datos.dat, una
vez escrita en pantalla la frase:
Quiero seguir aprendiendo C
sería:
De la misma manera que existe una función putc(), existe una función getc() que permite leer un carácter
de un fichero. La ventaja de la utilización de esta función para leer caracteres de un fichero es que no se detiene
cuando encuentra un salto de línea (‘\n’), frente a la función getchar(), que sí lo hacía. La función se detiene
cuando encuentra el indicativo final de fichero o EOF (véase sección 3.6, pág. 61). Como sucedía con putc, en la
función getc hay que introducir el puntero a fichero, como argumento.
Si se quiere realizar un programa que lea un texto de un fichero (con saltos de línea, incluidos) y lo imprima
en otro fichero distinto, un posible fragmento utilizando las funciones putc y getc, sería
En la LÍNEA 1, la función getc lee caracteres del fichero asociado al puntero: fent, hasta que encuentre el final
de fichero, representado con EOF. Esta construcción permite que se sigan leyendo caracteres, incluso si hay saltos
de línea en el fichero de entrada. En la LÍNEA 2, la función putc imprime el carácter en la salida establecida por
defecto. Esta salida, el C lo identifica a través del nombre stdout. Habitualmente es la pantalla. Hay también una
entrada por defecto, que suele utilizar la función getc. Se identifica por stdin y por lo general, es el teclado. En
la LÍNEA 3, se imprime el carácter en el fichero de salida, que está identificado con el puntero: fsal. Por último,
la variable cont es el contador de caracteres, que se va incrementando.
2. Condición bajo la cual el bucle continúa. Generalmente consiste en la evaluación de unos operadores de
relación (evaluación)
J. Sanguino
84 Capítulo – 4. Control del flujo
en el caso de una única sentencia, o bien entre llaves si deben ejecutarse un grupo de sentencias:
Interpretación: antes de iniciarse el bucle se ejecuta la inicialización, que consiste en una o más sen-
tencias que asignan valores iniciales a ciertas variables o contadores. A continuación se evalúa la condición.
Si el resultado es falso ( ⇐⇒ = 0) entonces prosigue en la siguiente línea ejecutable después del comando for
o bien, de la llave: } en el caso de tener un BLOQUE FOR. Si el resultado es verdadero ( ⇐⇒ 6= 0), entonces
se ejecuta la sentencia o sentencias que se encuentran entre llaves, en el BLOQUE FOR. Una vez acabada con la
última sentencia de este BLOQUE, se efectúa la actualización de la variable de control y se vuelve a com-
probar la condición. Si es verdadera, se repite la ejecución del bloque y se vuelven a realizar los procesos
de actualización y evaluación de la condición, así sucesivamente. En el momento que la condición sea falsa,
entonces el control del programa salta el BLOQUE FOR y continúa con la siguiente sentencia ejecutable. Aunque
en la inicialización se pueden asignar valores a distintas variables, en la actualización sólo se puede
modificar una. Ésta es la llamada variable de control del bucle.
Un ejemplo sencillo para ilustrar su funcionamiento se presenta en el código 4.12, que únicamente imprime
las cifras de 3 al 9 mediante una variable incremento.
Resultado de valor: 3
Resultado de valor: 4
Resultado de valor: 5
Resultado de valor: 6
Resultado de valor: 7
Resultado de valor: 8
Resultado de valor: 9
J. Sanguino
4.3 – Sentencias de repetición 85
Se observa en la LÍNEA 8, que este comando permite inicializar a la vez varias variables, que formarán parte de la
ejecución del bucle1 .
Un ejemplo más completo, consiste en sustituir el bucle while por el for, en el programa 4.6 (pág. 78),
como muestra el código 4.13
8
9 int suma = 0, numero;
10 int suma_Gauss = 0;
11
12 FILE *fi;
13
14 fi = fopen("Datos.dat", "w");
15
16 for(numero = 1; numero <= TOPE; numero++)
17 suma += numero; /* suma = suma + numero */
18
19 fprintf(fi, "La suma de los %d ", TOPE);
20 fprintf(fi, " primeros números es %d\n\n", suma);
21 fprintf(fi,"Veamos si me he equivocado: ");
22 fprintf(fi,"voy a aplicar la regla de Gauss");
23
24 suma_Gauss = TOPE * (TOPE + 1) / 2;
25 fprintf(fi, "\n \n suma_Gauss es %d ", suma_Gauss);
26 if (suma == suma_Gauss)
27 fprintf(fi, "\t Bien !!!! Sé sumar");
28
29 else
30 fprintf(fi, "\t Vaya !!!! Fallé, tengo que repasar");
31
32 fclose(fi);
33 return 0;
34 }
El resultado NO varía si en la parte de actualización del comando for, se sustituye la expresión sufija por
la prefija del operador incremental. Esto es, se cambian las LÍNEAS 16-17 por
J. Sanguino
86 Capítulo – 4. Control del flujo
do{
sentencia;
.
. /* <--- BLOQUE DO--WHILE */
.
sentencia;
} while (condición);
Interpretación: el bucle se ejecuta una vez, como mínimo, ya que el test se realiza tras la ejecución de la iteración.
El proceso es similar al bucle while anterior. Una vez que se llega a la condición, ésta se evalúa. Si el resultado
es falso ( ⇐⇒ = 0) entonces prosigue en la siguiente línea ejecutable. Si el resultado es verdadero ( ⇐⇒ = 6 0),
entonces se vuelve a ejecutar la sentencia o sentencias que se encuentran en el BLOQUE DO – WHILE. Una vez
acabada con la última sentencia de este BLOQUE, se vuelve a comprobar la condición. Si es verdadera, se
vuelve a repetir la ejecución del bloque y una vez terminado se comprueba la condición nuevamente y así
sucesivamente, hasta que la condición sea falsa, que entonces, el control del programa continúa con la siguiente
sentencia ejecutable. Para que esta estructura tenga sentido, al igual que sucedía con el bucle while, se suele poner
algún criterio dentro del BUCLE, que compruebe la condición, en cada ejecución del bucle.
Ejercicio:
Utilizando la instrucción do-while, realiza un pequeño programa en el que se introduzcan únicamente números naturales
entre el 0 y el 21.
N
A la vista de las diferentes opciones que ofrece el C para establecer un bucle, la pregunta clave es ¿qué bucle
elegir? En primer lugar hay que de decidir si se necesita un bucle con condición de entrada o salida. Lo habitual
es que el bucle necesite una condición de entrada. Kernighan y Ritchie [10, pág. 70] estiman que la condición de
salida es mucho menos utilizada. La opinión de este escaso uso es que suele ser mejor, mirar antes de saltar. Otra
razón de peso es que un programa es más legible, si la condición de test se encuentra al comienzo del bucle. No
hay que olvidar que existen aplicaciones en las que es importante que el bucle se evite si el test no se cumple en
un primer momento.
En cuanto a la utilización de los bucles for y while, la cosa no está tan clara. Generalmente, lo que puede
hacer uno de ellos, lo puede hacer con más o menos dificultad, el otro. Por lo que concierne al estilo, parece
más apropiado usar un bucle for cuando el bucle implique inicializar y actualizar una variable, tal y como se ha
planteado en el programa 4.13 (pág. 85). Sin embargo, para condiciones que no implican casos de incrementos,
tal y como sucede con el manejo de caracteres, tal vez sea mejor el bucle while, como muestra el código 4.11
(pág. 82)
J. Sanguino
4.5 – Números aleatorios 87
Este modelo de ejecución puede codificarse a partir de las instrucciones de control de flujo, estudiadas en este
capítulo. El algoritmo general es directo:
Selecciona;
haz mientras letra distinta ’q’
switch según letra y ejecuta función;
siguiente selección de letra;
fin haz mientras;
No obstante, puede dar lugar a codificaciones un tanto enrevesadas. En estas situaciones suele ser conveniente
agrupar diferentes tareas en funciones. Aunque el concepto de función en C no se explica hasta el capítulo 7,
no resulta muy complicado aplicar este concepto, a la confección elemental de un menú, siendo recomendable
consultar el Ejemplo: opciones de entrada, en la página 177. Utilizando como modelo esta codificación es bastante
directo diseñar un menú de entrada sencillo.
Los números a, b y m deben ser números enteros positivos, que se denominan respectivamente multiplicador,
incremento y módulo. El valor inicial n0 es la semilla y debe ser un dato inicial conocido.
Un inconveniente claro a este método y en general a todos los procesos de generación de números pseudo-
aleatorios es que no genera números aleatorios en absoluto. La idea en este caso, es tomar unos valores adecuados
para a, b y m de manera que obtención de los números sea lo más aleatoria posible. Una condición habitual es
tomar m suficientemente grande con m > a, b, n0 .
J. Sanguino
88 Capítulo – 4. Control del flujo
9 FILE *fi;
10
11 fi = fopen("Salida.dat", "w");
12
13 printf("Introduce la cantidad de números aleatorios -> ");
14 scanf(" %i", &NumMax);
15
16 fprintf(fi, "Los %i numeros aleatorios son: \n\n", NumMax);
17 for (cont = 1; cont <= NumMax; cont++){
18 numero = rand();
19 fprintf(fi, " %i ", numero);
20 }
21 fprintf(fi, "\n\n");
22 fprintf(fi, "******************\n\n");
23
24 fprintf(fi, "El rango del numero aleatorio es %lli", RAND_MAX);
25
26 fclose(fi);
27
28 return 0;
29 }
y cuya salida en el fichero salida.dat después de haber introducido en NumMax el valor de 10, sería la siguiente
******************
Los números obtenidos a través de la función rand() siguen una distribución uniforme (X ; U[0, RAND_MAX]),
con la idea que todos los números enteros que se generen en el intervalo [0, RAND_MAX] tengan la misma proba-
bilidad.
Es importante fijarse que en el programa anterior no ha sido necesario introducir ningún valor para la semilla. El
compilador de C ya proporciona una predefinida. Esto implica que si no se cambia, se generará siempre, la misma
secuencia de números pseudo–aleatorios. Sin embargo, el C permite modificar el valor de la semilla a través de
la función srand, que como argumento entra un valor entero, que es la semilla para proceso de generación. En el
código 4.15, se muestra un ejemplo de utilización de esta función, junto con la generadora rand(). La salida se
escribe en el fichero salida.dat.
6
7 int numero, NumMax, cont;
8 int semilla;
9
10 FILE *fi;
11
12 fi = fopen("Salida.dat", "a");
13
14 printf("Introduce la cantidad de números aleatorios -> ");
15 scanf(" %i", &NumMax);
J. Sanguino
4.5 – Números aleatorios 89
16
17 printf("Introduce el valor para la semilla -> ");
18 scanf(" %i", &semilla);
19
20 srand ( semilla );
21
22 fprintf(fi, "Los %i numeros aleatorios para la semilla %i son: \n\n",
NumMax, semilla);
23 for (cont = 1; cont <= NumMax; cont++){
24 numero = rand();
25 fprintf(fi, " %i ", numero);
26 }
27 fprintf(fi, "\n\n");
28 fprintf(fi, "******************\n\n");
29
30 fclose(fi);
31
32 return 0;
33 }
y cuya salida en el fichero salida.dat después de haber ejecutado varias veces el programa e introduciendo en
NumMax el valor de 10, sería la siguiente
******************
******************
******************
Como se observa en la salida, si el valor de la semilla no cambia (semilla = 2), la sucesión de números
pseudo–aleatorios coincide.
Si se quiere introducir un valor de la semilla, que no dependa del usuario, una posibilidad es que obtenga
este valor del reloj del sistema, a través de la función time. La definición de esta función está contenida en el
fichero time.h y por tanto hay que especificarla en el encabezamiento del programa. La utilización de este tipo
de semilla se hace mediante la instrucción
srand( time(NULL) );
Números enteros
Generalmente se necesita una secuencia de números enteros pseudo–aleatorios dentro de un intervalo dado.
Para ello es emplea el resto de la división entera. Por ejemplo, si se desea una sucesión de números pseudo–
aleatorios en el intervalo [1, 10] se podría operar de la forma:
numero = rand() % 10 + 1;
En general si se desea una sucesión de números pseudo–aleatorios en el intervalo [M, N ], se plantearía la opera-
ción
J. Sanguino
90 Capítulo – 4. Control del flujo
numero = rand() % (N - M + 1) + M;
Numeros reales
En ciertas ocasiones también es necesario generar una sucesión de números pseudo-aleatorios, pero reales.
Una manera sencilla de obtener números pseudo–aleatorios en el intervalo [0, 1] es dividir el resultado por la
constante RAND_MAX. Esto es, una operación del tipo
Si además se quisiera obtener los números pseudo–aleatorios reales en el intervalo [a, b], bastaría con las opera-
ciones
donde
1 1 2
gθ (θ) = y gr (r) = re− 2 r
2π
El siguiente paso es conseguir unos valores aleatorios para cada una de las funciones. En el caso de gθ resulta
1
inmediato, pues se genera una distribución Uniforme en el intervalo [0, 2π], con valor 2π . El segundo caso no es
tan inmediato, pero es posible definir la función
Z r
1 2 1 2
F (r) = t− 2 t dt = 1 − e− 2 r = 1 − u = v
0
con lo cual
r = F −1 (v) =
p
−2 log(1 − v)
J. Sanguino
4.6 – Tiempo de ejecución 91
donde u es el valor de una v.a. U ; U(0, 1). De esta manera se obtienen valores aleatorios para r y θ que generan
dos v.a. Normales independientes a partir de la ecuación (4.2). Así pues, el algoritmo Polar o de Box-Muller estaría
descrito de la forma
4. Calcula θ = 2πu2
Es destacable que este proceso genera dos números en cada iteración. Sin embargo, tiene el inconveniente de
√
utilizar las funciones ·, log, sen y cos. Esto resulta poco eficiente desde un punto de vista computacional. Una
alternativa es reducir las llamadas a estas funciones, evitando las trigonométricas, mediante un resultado básico de
trigonometría. En efecto si
v1
cos θ = pv 2 + v 2
1 2
(v1 , v2 ) ∈ (−1, 1) × (−1, 1) con 0 < v12 + v22 6 1 =⇒
v 2
sen θ = p 2
v1 + v22
4. Calcula
r
−2 log(s)
x = v1
s
r
−2 log(s)
y = v2
s
J. Sanguino
92 Capítulo – 4. Control del flujo
int main(void)
{
............
clock_t t_ini, t_final;
double Tiempo_total;
.......................
........................
........................
return 0;
}
La variable CLOCKS_PER_SEC tiene almacenado el número de ticks que se realizan en un segundo. Por tanto,
Tiempo_total es una variable real que almacena el número de segundos que se ha tardado en realizar la
ejecución.
4.7. Problemas
1. En la sección 3.8 de Problemas (pág. 65) se ha definido el Índice de Masa Corporal. Se pide que hagas un
programa que una vez calculado este coeficiente, tenga una salida que se ajuste al cuadro 4.1
J. Sanguino
4.7 – Problemas 93
2. Realiza un programa de forma que introducido un año, determine si es bisiesto o no. Para resolver este ejer-
cicio se puede consultar la página Leap Year de Wikipedia en inglés
(https://en.wikipedia.org/wiki/Leap_year) o bien en español, año bisiesto también de Wi-
kipedia.
3. Realiza un programa que escriba en un fichero, una tabla con las potencias cuadradas y cúbicas de los n
primeros números naturales, donde n debe ser seleccionado desde el teclado (ha de cumplirse que n 6 100).
Este resultado debe escribirse en un fichero llamado Datos.dat. Una posible ejecución del programa
podría ser
1
1+
1
1+
..
.
..
.
1
1+
1
1+
1
1+
1
1+
1+1
La entrada del programa debe ser un valor entero que determine la cantidad de términos que deben calcularse.
√
Se trata de una sucesión que se aproxima al número áureo o razón áurea: (1 + 5)/2. A continuación se
pide que hagas un programa que dado un valor (por ejemplo: 10−6 ), se determine el número de términos de la
sucesión que se necesitan, para obtener una aproximación de la razón áurea, menor que el valor introducido.
En la salida también debe darse esta aproximación.
(Propuesto en examen: curso 16/17)
J. Sanguino
94 Capítulo – 4. Control del flujo
5. Escribe un programa que determina todos los divisores de un numero natural y el resultado lo escriba en un
fichero llamado Datos.dat. Una posible ejecución podría ser:
6. Utilizando lo comentado en la sección 4.3.1 (pág. 78) con las funciones getc y putc escribe un programa,
que lea un texto de un fichero de entrada (por ejemplo Entrada.dat) y escriba dicho texto, en un fichero
de salida (por ejemplo Salida.dat). Se debe poder contar el número de caracteres del texto (incluidos
los blancos). Por ejemplo, el fichero Entrada.dat podría contener el texto:
7. Utilizando un bucle for, escribe un programa que calcule el factorial de un número natural n, introducido
por el teclado. Ten en cuenta, que como consecuencia del ejercicio planteado en la sección 3.8 de Problemas,
el valor del factorial para números mayores de 21 no es muy precisa3 , debido a un posible desbordamiento
(overflow). Una posible ejecución del programa podría ser:
y el resultado
15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 = 15 ! = 1307674368000
J. Sanguino
4.7 – Problemas 95
Haz un programa que calcule el número combinatorio correctamente, dados dos números naturales m y n
que entren por teclado y en cualquier orden4 .
11. Mediante la instrucción do-while, haz un programa que compruebe si un número introducido por teclado
es un entero sin signo. Si lo es, que lo imprima. En caso contrario que vuelva a pedirlo por teclado.
12. En la sección 2.7 de Problemas (pág. 22) se planteó el Algoritmo de Euclides. Ahora se pide que programes
este algoritmo con la instrucción do-while.
13. Escribe un programa que determine si un número entero positivo n es primo o no. Por lo general, basta con
√
comprobar si todos los números menores o iguales que n, dividen a n.
14. Utilizando una estructura switch escriba un programa que dados dos números enteros positivos, permita
elegir al usuario, si obtener Vm,n , V Rm,n o m
n . El programa terminará cuando se introduzca el carácter @.
15. Realiza un programa que imprima un cuadrado de asteriscos en un fichero llamado Datos.dat. El dato
de entrada n, será el tamaño del lado. Por tanto el cuadrado contendrá n × n asteriscos. Un ejemplo de
ejecución sería:
*****
*****
*****
*****
*****
a) Con la misma idea que en el caso anterior, ahora que imprima, para un tamaño n, un triángulo de
forma:
&
&&
&&&
&&&&
&&&&&
&&&&&&
(en este caso n = 6)
b) Ahora un triángulo de la forma
4
Este programa se puede simplificar definiendo el concepto de función que se verá más adelante
J. Sanguino
96 Capítulo – 4. Control del flujo
$$$$$$
$$$$$
$$$$
$$$
$$
$
(n = 6)
c) El siguiente con la forma
######
#####
####
###
##
#
(n = 6)
d) Por último, una pirámide de la forma:
@
@@@
@@@@@
@@@@@@@
@@@@@@@@@
@@@@@@@@@@@
(n = 6)
16. Se llama triángulo de Floyd al triángulo rectángulo formado con números naturales. Para crear un triángulo
de Floyd, se comienza con un 1 en la esquina superior izquierda, y se continúa escribiendo la secuencia de
los números naturales de manera que cada línea contenga un número más que la anterior. Por ejemplo, el
triángulo de Floyd para n = 4 sería:
1
2 3
4 5 6
7 8 9 10
Se pide que hagas un programa que dado un número n (tamaño del cateto), se imprima el triángulo de Floyd,
en un fichero.
17. Realiza un programa que implemente el algoritmo planteado en (4.1) y obtenga 20 números aleatorios. La
salida debe realizarse a un fichero llamado Salida.dat y se debe poder guardar los resultados obtenidos
con ejecuciones anteriores. Prueba con los valores a = 5, b = 3, m = 16 y n0 = 7. Repite la ejecución
cambiando los valores de la semilla.
18. Realiza un programa que determine los n números enteros pseudo-aleatorios en el intervalo [M, N ] cuyos
extremos deben introducirse por teclado y la salida debe escribirse en un fichero. Se debe introducir también
el valor de la semilla y observar en el fichero las diferentes sucesiones obtenidas para diferentes semillas.
Implementar la obtención de la semilla mediante la lectura del reloj del sistema.
19. Realiza un programa que determine los n números reales pseudo-aleatorios en el intervalo [a, b] cuyos
extremos deben introducirse por teclado y la salida debe escribirse en un fichero. Se debe introducir también
el valor de la semilla y observar en el fichero las diferentes sucesiones obtenidas para diferentes semillas.
Implementar la obtención de la semilla mediante la lectura del reloj del sistema.
J. Sanguino
4.7 – Problemas 97
20. Realiza un programa que simule el lanzamiento de una moneda 1.000.000 de veces y obtener la frecuencia
absoluta para cada una de las distintas caras.
21. Realiza un programa que simule el lanzamiento de un dado 6.000.000 de veces y obtener la frecuencia
absoluta para cada una de las distintas caras. ¿Cuál sería la probabilidad de cada una de las caras?
22. Utiliza uno de los dos algoritmos de Box-Muller planteados en teoría, para simular un conjunto de números
con distribución Normal N (µ, σ) cuyos valores µ y σ deben introducirse, así como el número de datos que
se deben generar.
23. Comprueba, tal y como dice en la teoría, que el segundo algoritmo de Box-Muller es más eficiente que el
primero.
24. Anteriormente has realizado un programa que calculaba la tabla de los cuadrados y cubos de los n primeros
números naturales. Amplía este programa para que también te pida la potencia. Por ejemplo, una posible
ejecución podría ser:
y la salida
25. Se pide que a partir de un intervalo dado se generen una cierta cantidad de números aleatorios enteros:
x1 , . . . , xn y con ellos calcules la media y la cuasi-desviación típica muestral, esto es:
n √
1X
x= xi y s= s2
n
i=1
donde
n
!
1 X
s2 = x2i − nx2
n−1
i=1
J. Sanguino
98 Capítulo – 4. Control del flujo
b) los extremos del intervalo (que deben ser valores enteros), en cualquier orden.
c) semilla (un valor entero), para generar diversas muestras de números pseudo–aleatorios.
La media es 5.690000000
La CuasiDesviación Típica muestral es 2.751289872
se pide que realices un programa que determine el mayor número natural n0 tal que se cumpla:
n
1
x=1+ >1
2
El programa debe permitir al usuario seleccionar si el cálculo se realiza en simple precisión, tecleando la
letra S o en doble precisión, tecleando la letra D.
Una ejecución típica del programa con salida en pantalla sería:
El valor de n0 es: 23
El valor de 1/2 elevado a n0 es: 0.0000001192
J. Sanguino
4.7 – Problemas 99
27. El objetivo de este problema es que realices un programa que aproxime la raíz cúbica de un número a. Esto
es, dado a se cumpla: √
3
a = x ⇐⇒ a = x3
El método de Newton-Raphson permite establecer un procedimiento iterativo que genera una aproximación
a la raíz cúbica de un número dado. Este procedimiento se asocia a un algoritmo iterativo de punto fijo, cuya
función de iteración viene dada mediante:
1 a
g(x) = 2x + 2 ,
3 x
que permite generar un proceso iterativo de la forma:
1 a
x0 = 1; xn+1 = g(xn ) = 2xn + 2 con n = 0, 1, 2, . . .
3 xn
|a − x3n | < ε
En la pantalla:
Introduce un número real -> -27
Introduce el error (0 < error < 1) -> -34
Introduce el error (0 < error < 1) -> 1.5
Introduce el error (0 < error < 1) -> .01
En un fichero de salida:
El número de iteraciones es 6
xn+1 = f (xn )
J. Sanguino
100 Capítulo – 4. Control del flujo
Es una conjetura (llamada la conjetura de Collatz), que esta sucesión siempre alcanza el valor 1, sea cual
sea el número natural inicial.
Se llama órbita de la sucesión, al número de términos que se generan hasta llegar al primer 1.
Se debe codificar f , descrita en (4.3), mediante una función llamada Collatz. Esta función debe devolver
cada término de la sucesión al programa principal, desde donde se escribirán estos términos, en el fichero de
salida. La generación de los términos de la sucesión parará cuando se alcance el primer 1. Además, el pro-
grama debe imprimir en el fichero de salida la órbita del número natural (n > 1), introducido inicialmente.
Una ejecución típica del del programa sería
En la pantalla:
Introduce un número natural (n > 1) -> -5
Introduce un número natural (n > 1) -> 1
Introduce un número natural (n > 1) -> 6
En el fichero de salida:
x x2 x3 xn
ex = 1 + + + + ··· + + ···
1! 2! 3! n!
Se pide que realices un programa, codificado en C, que aproxime el valor e−1 (que se tomará como e−1 =
0.367879441) mediante la serie de Taylor anterior:
1 1 1 1
e−1 = 1 − + − + · · · + (−1)n + · · · n = 0, 1, 2, . . .
1! 2! 3! n!
Se utilizará una función llamada Sucesion, que devolverá el número de iteraciones necesarias, para apro-
ximar el valor e−1 . Uno de los argumentos será el error con el que se quiere aproximar dicho número:
10−1 , 10−2 , 10−3 , . . .. Este número de iteraciones se escribirá en el fichero de salida. Además dentro de la
función anterior se definirá una constante que almacene el valor de e−1 y se escribirá en el fichero de salida,
el valor de e−1 = 0.367879441, junto con el valor de la aproximación.
Las variables reales necesarias para la codificación deberán tener, a lo sumo, 6 ó 7 cifras significativas.
Una ejecución típica del del programa sería
En la pantalla:
J. Sanguino
4.7 – Problemas 101
El número de iteraciones es 6
30. Sea una circunferencia de radio 1 inscrita en el cuadrado [−1, 1] × [−1, 1]. Para determinar el valor aproxi-
mado de π se utiliza el siguiente procedimiento: se generan aleatoriamente N puntos en el cuadrado anterior:
[−1, 1] × [−1, 1]. De estos N puntos, se cuentan los N0 que estén en el círculo x2 + y 2 < 1. Pues bien, el
valor aproximado de π viene dado por la expresión
N0
π≈4 (4.4)
N
Por lo general, la aproximación a π, tiende mejorar a medida que se aumenta la cantidad de números aleato-
rios N .
La medida de la aproximación se realiza mediante el valor absoluto de la diferencia entre el valor exacto de π
(que se tomará como PI = 3.141592653589793238) y el valor obtenido mediante la expresión (4.4),
para diferentes valores de N .
Se pide que realices un programa, codificado en C, que calcule el valor aproximado de π con un error menor
que ε = 0.001. Se comenzará con el valor N = 100 y se irá incrementando en factores de 10, hasta obtener
un error menor que el valor ε dado. Como cota superior para N , se tomará N = 108 .
NORMAS
a) La entrada será la semilla para la generación de números aleatorios, que debe corresponder a un entero
negativo.
b) Se utilizará una función de tipo entero (int), llamada CuentaDentroCirc que devolverá al pro-
grama principal, el valor N0 . Debe tener dos argumentos: N (la cantidad de números aleatorios que
deben generarse en el cuadrado [−1, 1] × [−1, 1]) y S (la semilla para generar los números aleatorios).
c) En el programa principal se calculará el error de la aproximación como diferencia de la constante real
PI = 3.141592653589793238 y el valor aproximado, obtenido de la expresión (4.4). Para ello
se definirá la función absoluto definida por el usuario mediante el operador condicional (tal y como
se ha realizado en clase) y NO se debe utilizar la función fabs de la biblioteca estándar
d) Las variables reales necesarias deberán tener, al menos 10 cifras significativas.
e) Debe crearse una función externa llamada presentacion que imprima en la pantalla, los datos del
alumno: Nombre, Apellidos y Número de matrícula.
f ) El resultado deben escribirse en un fichero llamado Salida.dat tal y como se plantea en el ejemplo,
a continuación.
J. Sanguino
102 Capítulo – 4. Control del flujo
En la pantalla:
En el fichero:
El error introducido es 0.001000; La semilla es -3
N = 100 PI_aprox = 2.960000000000000 Error = 0.181592653589793
N0
I≈ A (4.5)
N
donde A = (b − a) fmax , que en este caso A = 1.
Por lo general, la aproximación a I, tiende mejorar a medida que se aumenta la cantidad de números aleato-
rios N .
La medida de la aproximación se realiza mediante el valor absoluto de la diferencia entre el valor exacto de
I (que se tomará como I = 0.3333333333333333) y el valor obtenido mediante la expresión (4.5),
para diferentes valores de N .
Se pide que realices un programa, codificado en C, que calcule el valor aproximado de I con un error menor
que ε = 0.0001. Se comenzará con el valor N = 100 y se irá incrementando en factores de 10, hasta obtener
un error menor que el valor ε dado. Como cota superior para N , se tomará N = 108
NORMAS
J. Sanguino
4.7 – Problemas 103
a) La entrada será la semilla para la generación de números aleatorios, que debe corresponder a un entero
positivo.
b) Se utilizará una función de tipo entero (int), llamada CuentaDentroArea que devolverá al pro-
grama principal, el valor N0 . Debe tener dos argumentos: N (la cantidad de números aleatorios que
deben generarse en el cuadrado [0, 1] × [0, 1]) y S (la semilla para generar los números aleatorios).
c) En el programa principal se calculará el error de la aproximación como diferencia de la constante real
I = 0.3333333333333333 y el valor aproximado obtenido de la expresión (4.5). Para ello se
definirá la función absoluto definida por el usuario mediante el operador condicional (tal y como
se ha realizado en clase) y NO se debe utilizar la función fabs de la biblioteca estándar
d) Las variables reales necesarias deberán tener, al menos 10 cifras significativas.
e) Debe crearse una función externa llamada presentacion que imprima en la pantalla, los datos del
alumno: Nombre, Apellidos y Número de matrícula.
f ) El resultado deben escribirse en un fichero llamado Salida.dat tal y como se plantea en el ejemplo,
a continuación.
En la pantalla:
k !! = k · (k − 2) · (k − 4) · · · 4 · 2
si k es par y
k !! = k · (k − 2) · (k − 4) · · · 3 · 1
J. Sanguino
104 Capítulo – 4. Control del flujo
Se pide que realices un programa, codificado en C, que calcule el valor aproximado de estas integrales a
partir de la expresión (4.6), utilizando el doble factorial y tomando el valor de π como una constante, dada
por PI = 3.141592653589793238.
NORMAS
a) La entrada serán las potencias m y n que deben ser números enteros positivos mayores que 1, respec-
tivamente.
b) Se utilizará una función de tipo entero (long long unsigned), llamada DobleFactorial que
devolverá al programa principal dicho valor.
c) En el programa principal se calculará el valor de la integral I.
d) Las variables reales necesarias deberán tener, al menos 10 cifras significativas y las enteras deben ser
de tamaño adecuado para que el factorial de un resultado fiable para el mayor natural posible.
e) Debe crearse una función externa llamada presentacion que imprima en la pantalla y en el fichero
de salida, los datos del alumno: Nombre, Apellidos y Número de matrícula.
f ) Los resultados deben escribirse en un fichero llamado Salida.dat tal y como se plantea en el
ejemplo, a continuación.
Una ejecución típica del programa sería
En la pantalla:
**********************
J. Sanguino
4.7 – Problemas 105
x3 x5 x7 x2n−1
arctan(x) = x − + − + · · · + (−1)n+1 + ··· n = 1, 2, . . . con 1 < x 6 1
3 5 7 2n − 1
π
Se pide que realices un programa, codificado en C, que aproxime el valor arctan(1) = 4 (que se tomará
como π4 = 0.7853982) mediante la serie de Taylor anterior:
1 1 1 1
arctan(1) = 1 − + − + · · · + (−1)n+1 + ··· n = 1, 2, . . .
3 5 7 2n − 1
Para ello:
a) Se utilizará una función llamada Sucesion, que devolverá al programa principal, el error obtenido,
para aproximar el valor π4 , desde donde se escribirá en el fichero de salida. Uno de los argumentos será
el número de iteraciones con la que se quiere aproximar dicho número: 10, 100, 500, . . ..
b) dentro de la función anterior se definirá una constante que almacene el valor de π4 . Además dentro de
esta función se escribirá en el fichero de salida, el valor de π4 = 0.7853982 y el valor de la aproxima-
ción.
c) las variables reales necesarias deberán tener, a lo sumo, 6 ó 7 cifras significativas.
d) para determinar el valor de la aproximación es necesario utilizar la función valor absoluto. Ésta deberá
programarse como función externa mediante el operador condicional y NO utilizar la función fabs
de la biblioteca estándar.
e) la ejecución del programa sólo se detendrá si el usuario introduce el valor 0, de otra forma el programa
pedirá un nuevo valor para el error y realizar el proceso anteriormente descrito.
f ) debe crearse una función externa llamada presentacion que imprima en la pantalla y en el fichero
de salida, los datos del alumno: nombre, apellidos y número de matrícula.
En la pantalla:
J. Sanguino
106 Capítulo – 4. Control del flujo
34. Se pide que a partir de un intervalo dado se generen una cierta cantidad de números aleatorios enteros:
x1 , . . . , xn y con ellos se estime el valor de la media mediante el promedio móvil ponderado exponencial-
mente (EWMA), expresado por: Pn n−i x
i=1 λ i
µ
b= P n n−i
i=1 λ
siendo λ = 0.95 un valor constante. El programa debe permitir al usuario seleccionar:
Además:
a) se debe definir una función real llamada Potencia, que calcule la potencia de un número real pero
con exponente no negativo, que se utilizará para determinar el valor de λn−i .
b) las variables reales deben tener, al menos, 10 cifras significativas.
c) el resultado se escribirá en un fichero, llamado Salida.dat, desde el programa principal.
d) la ejecución del programa sólo se detendrá si el usuario introduce el valor 0, de otra forma el programa
pedirá una nueva cantidad de números aleatorios para realizar el proceso anteriormente descrito.
e) se debe disponer de los datos del alumno en la pantalla como en fichero de salida, a través de una
función llamada presentacion.
En la pantalla:
***** Nombre: Jdkdkdkd
**** Apellidos: Skdkdkd Rdkdkdkd
**** Matricula: 1111111
***************************
J. Sanguino
4.7 – Problemas 107
La media es 2.962196853
*****************
La media es 2.282587518
*****************
35. Se pide que realices un código en C consistente en dado un número natural, generar los factoriales de todos
los enteros no negativos menores e iguales que el número dado. Para ello:
se debe crear una función llamada Facto cuyo argumento será un entero no negativo y la salida el
factorial de dicho número con un tipo long long unsigned.
se debe crear una función llamada FactoD cuyo argumento será un entero no negativo y la salida el
factorial de dicho número con un tipo double.
la introducción de datos se realizará generando números enteros no negativos aleatorios. Esto es, el
usuario introducirá la cantidad de números junto con los extremos del intervalo y la semilla. A partir
de ese momento, el programa deberá generar esta cantidad de números aleatorios no negativos, para
los que ejecutará el programa (generará los respectivos factoriales).
la salida será en un fichero de salida, creado a tal efecto llamado Salida.dat. En la salida se de-
ben generar dos columnas, en donde en una de ellas aparecerán los factoriales de tipo long long
unsigned y en la otra, los de tipo double, tal y como se ha visto en clase.
para generar la salida pedida en el apartado anterior, se debe llamar a las funciones: Facto y FactoD
desde el programa principal e imprimir los resultados, también desde el programa principal.
se debe realizar un control al dato de entrada. Esto es, debe poder introducirse de nuevo si el valor
tecleado por el usuario es negativo o un carácter.
J. Sanguino
108 Capítulo – 4. Control del flujo
deben aparecer los datos del alumno: apellidos, nombre y número de matrícula tanto, por pantalla,
como escribirse en el fichero. Esto debe hacerse mediante una función definida al efecto, llamada
presentacion (tal y como se ha estado realizando en clase).
En la pantalla:
Factoriales para 5:
0! = 1 0! = 1.0
1! = 1 1! = 1.0
2! = 2 2! = 2.0
3! = 6 3! = 6.0
4! = 24 4! = 24.0
5! = 120 5! = 120.0
Factoriales para 1:
0! = 1 0! = 1.0
J. Sanguino
4.7 – Problemas 109
1! = 1 1! = 1.0
36. Se pide que realices un código en C consistente en dado un número natural en base decimal se pueda
expresar en binario y que a partir de este binario, se vuelva a recuperar la expresión en decimal.
Para ello:
se debe crear una función llamada Binario cuyo argumento será un número en decimal y la salida
su valor en binario. Dicho valor debe almacenarse en una variable (long long unsigned), que
se imprimirá en la pantalla y en un fichero, desde el programa principal. Para obtener un número en
binario se aplica el algoritmo (visto en clase):
ri = ci %2
i = 1, . . . , k
c = c /2
i+1 i
la ejecución del programa sólo se detendrá si el usuario introduce el valor 0, de otra forma el programa
pedirá un nuevo valor positivo en decimal para realizar el proceso anteriormente descrito.
se debe realizar un control al dato de entrada. Esto es, debe poder introducirse de nuevo si el valor
tecleado por el usuario es negativo o un carácter.
la salida debe poder realizarse tanto por pantalla, como en un fichero que se llamará Salida.dat.
deben aparecer los datos del alumno: apellidos, nombre y número de matrícula tanto, por pantalla,
como escribirse en el fichero. Esto debe hacerse mediante una función definida al efecto, llamada
presentacion (tal y como se ha estado realizando en clase).
En la pantalla:
J. Sanguino
110 Capítulo – 4. Control del flujo
....
....
En el fichero:
11 en base 2 es 1011
1011 en base 10 es 11
J. Sanguino
CAPÍTULO 5
Datos estructurados
5.1. Introducción
Dentro del conjunto de de los datos estructurados que se pueden definir en C, los más sencillos son los vectores
y matrices. Desde el punto de vista informático, un vector es un conjunto de datos del mismo tipo y bajo un mismo
nombre, cuyos elementos pueden localizarse independientemente por medio de un índice. El concepto de matriz
es análogo al de vector, con la única diferencia que los datos se localizan mediante dos índices.
En general, en informática, para referirse a este tipo de estructuras de datos, e incluso a aquellas para los que es
necesario la utilización de más de dos índices, se utiliza la palabra array. Así, los arrays de un índice corresponde
a la idea de vectores, mientras que los arrays de dos índices correspondería a las matrices. Los arrays de más
de dos índices, tienen una difícil traducción en castellano1 . En este documento se utilizará únicamente la palabra
array, cuando sea necesario la utilización de más de dos índices. Los vectores, matrices y arrays, constituyen unos
datos estructurados llamados homogéneos, puesto que todos sus datos son del mismo tipo.
En C, se pueden definir unos datos estructurados más complejos en los que es posible mezclar diferentes
tipos de datos. Se conocen como estructuras y por tanto, son unos datos estructurados heterogéneos. Con esta
modalidad, el programador puede definir su propio tipo de variable, proporcionando una gran flexibilidad.
En este capítulo, se estudiará de qué manera el lenguaje C representa y opera con estas estructuras de datos.
5.2. Vectores
En matemáticas, los vectores en espacios de dimensión finita se pueden asociar a n-uplas de números reales.
Esto es,
v = [a1 , a2 , . . . , an ] con ai ∈ R
Como ya se ha dicho, en informática y por tanto, en C la idea de vector es similar, pero algo más general; sus ele-
mentos, por ejemplo, no tienen por qué ser números, pueden ser caracteres, y dar lugar a una cadena de caracteres.
Los vectores en C tienen un tratamiento similar a las variables: se declaran, se inicializan, se modifican, se
operan, etc. La manera de declarar un vector es
tipo nombre[número_de_elementos];
111
112 Capítulo – 5. Datos estructurados
2. Cada componente del vector es a su vez una variable del mismo tipo que el utilizado en la declaración.
3. El tamaño de memoria que se reserva para las variables vectoriales, se calcula como producto del tamaño
del tipo (int (4 bytes), float (4 bytes), double (8 bytes), char (1 byte), . . . ) por la dimensión (número
de elementos).
4. En C los elementos se numeran desde el 0 hasta número_de_elementos−1.
Por ejemplo:
En la LÍNEA 3, se reserva un espacio de 30 variables de tipo int, asociadas al mismo nombre de vec_ent. Esto
quiere decir que en memoria se reservan 30 × 4 = 120 bytes consecutivos, para estas 30 variables. La dimensión
de esta variable vectorial, se establece a partir de la constante Dim. La numeración de las variables empieza en el
0 y termina en el 29. Cada variable se representa por el nombre y entre corchetes, el valor del índice. Esto es, se
tendría las siguientes variables enteras
vec_ent[0], vec_ent[1], ..., vec_ent[29]
De manera análoga, en la LÍNEA 4 se reserva un espacio para 15 variables reales double, con el nombre de
vec_real y en la última línea se define un vector de nombre frase y de tamaño 25 caracteres (o bytes: cada
carácter se almacena en un byte)).
Para acceder a un elemento del vector, basta con incluir en una expresión su nombre seguido del índice en-
tre corchetes. En C, no se puede operar con todo un vector o matriz como única entidad (otros lenguajes como
MATLAB , sí lo permiten), se debe tratar cada una de sus componentes de manera independiente. Esto implica la
utilización de bucles, generalmente for o while, para que todos las componentes se vean afectadas de la misma
forma.
En C las componentes de los vectores, se utilizan como cualquier otra variable (de hecho, son variables). Son
perfectamente factibles las siguientes expresiones:
vec_real[7] = 12.56;
vec_real[0] = vec_real[26] * vec_real[18];
vec_real[24] = vec_real[13]/vec_real[7] + 23.8 * vec_real[21];
vec_real[20] = vec_real[20] + 1;
En la primera línea se inicializa cada elemento del vector enteros por los valores considerados, de esta forma
se tiene:
enteros[0] = 21; enteros[1] = 34; · · · ; enteros[6] = 40
En la siguiente, mediante esta inicialización la dimensión de reales[3], queda implícita. Con la inicialización
que viene a continuación, C considera que el resto de los componentes del vector reales_cortos, que no se
inicializan explícitamente, son 0. Así pues, para hacer 0 todas las componentes del vector reales_cortos,
hubiese bastado con la declaración:
J. Sanguino
5.2 – Vectores 113
En la última línea, se muestra cómo se pueden inicializar sólo algunas de las componentes del vector.
Un programa básico sobre la inicialización y operaciones con un vector puede verse en el código 5.1
Hay que insistir que las componentes de un vector están constituidas por variables de un mismo tipo. Por tanto,
todo lo visto hasta ahora, referente a variables es de aplicación a cada una de las componentes de un vector. De esta
forma, también es posible introducir los valores de las componentes de un vector a través del teclado, mediante la
función scanf, tal y como se ilustra en el código 5.2. En este caso se introducen los valores de las componentes
y el resultado se imprime en el ficheros Datos.dat.
3 int main(void)
4 {
5 const int LIM = 5;
6 int Vec[LIM];
7 unsigned int i;
8
9 FILE *fi;
10
11 fi = fopen("Datos.dat", "w");
12
J. Sanguino
114 Capítulo – 5. Datos estructurados
27 return 0;
28 }
La ejecución del programa sería (se introducen por el teclado los números: 23, −4, 0, −123, 42)
Vect[1] = 23
Vect[2] = -4
Vect[3] = 0
Vect[4] = -123
Vect[5] = 42
No es frecuente introducir las componentes de un vector a través del teclado, sobre todo si el número de
componentes es elevado. Lo más habitual es leer los valores de un fichero, en el que previamente se han introducido
por la acción de cierto proceso anterior. Mediante la función fscanf, se pueden leer valores de variables en
ficheros. Su estructura y funcionamiento es similar a la función fprintf, que ya ha sido utilizada en programas
anteriores. La diferencia fundamental con la función scanf es que hay que añadir una nueva componente a su
argumento, que corresponderá al identificador del fichero, como ocurre con fprintf.
En el código 5.3, se plantea un ejemplo sencillo en el que se leen los valores de las componentes de un vector.
Posteriormente se almacenan en un fichero llamado Fichero.dat. Por último, se leen de este fichero los datos
introducidos y se muestran en la pantalla.
J. Sanguino
5.2 – Vectores 115
La lectura de los datos desde el fichero Fichero.dat se realiza, mediante un bucle for en la LÍNEAS 28-30. Es
importante señalar que primeramente se ha abierto (o creado) el fichero en modo escritura, mediante el modo w ,
como se observa en la LÍNEA 11 (véase sección 3.6, Ficheros pág. 61). Pero para leer los datos almacenados, se ha
debido cerrar (LÍNEA 26) y volver abrir previamente, en el modo r (LÍNEA 27). Esto se hace, para que el cabezal
del fichero se vuelva a posicionar al principio de fichero y pueda leer los datos desde el comienzo del fichero.
Además se debe abrir con la opción r , porque con w hubiese borrado el contenido, previamente almacenado.
Un ejemplo de código que genera los términos de la sucesión de Fibonacci se presenta en el programa 5.4. En este
código se pide un número N mayor que 2 pero menor que 30 y genera los N primeros números de la sucesión.
9 int N;
10 unsigned long long TablaFibo[30] = {0ULL ,1ULL};
11 unsigned int ind;
12
J. Sanguino
116 Capítulo – 5. Datos estructurados
13 FILE *fi;
14
15 fi = fopen("Datos.dat", "w");
16
17 printf("\n\n **** Este programa determina la tabla de los número de Fibonacci
****\n\n");
18 printf("Introduce un entero 2 < N < 30 -> ");
19 while(scanf(" %i", &N) != 1 || N < 3 || N > 29){
20 while(getchar() != ’\n’); /* limpia el flujo de entrada de datos*/
21 /*Se introduce de nuevo el numero */
22 printf("\n\nHas de introducir entero positivo 2 < n < 30 -> ");
23 }
24 while(getchar() != ’\n’);
25
En la LÍNEA 28 se aprecia cómo para obtener el siguiente término de la sucesión a partir de los dos anteriores,
únicamente hay que operar con los índices de las componentes del vector TablaFibo.
15 Min = Vec[0];
16 Pos = 0;
17 for (i = 1; i < DIM; i++){
18 if (Vec[i] < Min){
J. Sanguino
5.2 – Vectores 117
19 Min = Vec[i];
20 Pos = i;
21 }
22 }
23 fprintf(fi, "El valor mínimo del vector: \n");
24 for(i = 0; i < DIM; i++){
25 fprintf(fi, " %.4lf ", Vec[i]);
26 }
27 fprintf(fi,"\n");
28 fprintf(fi,"es %.4lf y ocupa la posición %u \n", Min, Pos+1);
29
30 fclose(fi);
31
32 return 0;
33 }
se puede operar computacionalmente con ella, guardando sus coeficientes ai en las componentes de un vector de
tamaño n + 1. Una operación bastante habitual es evaluar dicha función polinómica en un punto dado x0 , esto es
Su expresión en C sería:
1 punto = x0;
2 for (valor = a[0], i = 1; i <= n; i++){
3 valor += a[i]*pow(punto, i);
4 }
donde pow es la función potencia de la biblioteca math.h. Sin embargo, resulta más eficiente desde el punto de
vista del número de operaciones, proceder de la siguiente manera:
1 punto = x0;
2 for (valor = a[n], i = n-1; 0 <= i; i--){
3 valor *= punto;
4 valor += a[i];
5 }
En este caso se realizan n adiciones y n productos, mientras que en el procedimiento clásico (5.1) se realizan n
sumas y 2n − 1 productos, es decir n − 1 multiplicaciones de más. Se trata de un procedimiento similar a la Regla
de Ruffini que es utilizada en cursos básicos, para evaluar un polinomio en un punto dado.
J. Sanguino
118 Capítulo – 5. Datos estructurados
En el código 5.6 se muestra un ejemplo en el que se utiliza el algoritmo de Horner, para evaluar el polinomio
en el punto x0 = −1.6.
7 int main(void)
8 {
9 int i;
10 double pol[DIM] = {2.4, 4.5, -23.5, 2.56, 7.95};
11 double x0 = -1.6;
12 double valor, valor2;
13
14 FILE *fi;
15 fi = fopen("Datos.dat", "w");
16
17 valor = pol[DIM-1]; //pol[DIM-1] = a_{n}
18 for (i = DIM - 2; 0 <= i; i--){
19 valor *= x0;
20 valor += pol[i];
21 }
22
23 fprintf(fi, " Valor con Horner: %.9f\n", valor);
24
31 fclose(fi);
32
33 return 0;
34 }
En este programa los coeficientes del polinomio se introducen a través de un vector de tipo double, llamado
pol (LÍNEA 10) y que se inicializa con los valores deseados. El algoritmo de Horner codifica en las LÍNEAS 17-21.
El método tradicional de evaluación, se presenta en las LÍNEAS 25-27. El resultado de la ejecución del programa 5.6
se almacena el fichero Datos.dat y contiene, para los datos de entrada (LÍNEAS 10-11):
J. Sanguino
5.3 – Cadenas de caracteres 119
M a r í a J o s é \0
Tamaño de 11 posiciones
Las cadenas de caracteres tienen su propio especificador de formato, que es s. Con lo cual si se quiere escribir
una cadena, se pondría:
Si se quiere introducir una cadena de caracteres se emplearía la función scanf, como muestra el ejemplo
aunque existe una pequeña diferencia con los casos ya vistos, en capítulos anteriores. Ahora ya no es necesario la
utilización del operador dirección: &, porque el nombre de la cadena, constituye un puntero3 . No obstante, a la
hora de introducir una cadena de caracteres por medio del teclado, es más adecuado utilizar la función gets. La
diferencia es que scanf se para en el momento de localizar un blanco y gets se detiene cuando encuentra un
retorno de carro. Su utilización es muy simple, como se observa en el siguiente ejemplo:
Función strlen
Es una función que devuelve la longitud de una cadena de caracteres. Su sintaxis es strlen(cadena).
J. Sanguino
120 Capítulo – 5. Datos estructurados
2 #include <string.h>
3 /* Ejemplo de programa con cadenas de caracteres */
4
5 int main(void)
6 {
7 char fraseguay[] = "Me encantan las clases de C";
8
9 FILE *fi;
10 fi = fopen("Datos.dat", "w");
11
12 fprintf(fi, "La frase es: / %s /\n", fraseguay);
13 fprintf(fi, " y está formada por %d caracteres", strlen(fraseguay));
14
15
16 fclose(fi);
17
18 return 0;
19 }
Función strcat
J. Sanguino
5.3 – Cadenas de caracteres 121
Función strcmp
1: De izda. a decha. hay algún carácter en la cadena1 que es posterior a la cadena2. Por tanto, no están
ordenadas alfabéticamente.
-1: De izda. a decha. hay algún carácter en la cadena1 que es anterior a la cadena2. Por tanto, están
ordenadas alfabéticamente.
10
11 fprintf(fi, "A-A: %d\n", strcmp("A", "A"));
12 fprintf(fi, "A-B: %d\n", strcmp("A","B"));
13 fprintf(fi, "B-A: %d\n", strcmp("B","A"));
14 fprintf(fi, "C-A: %d\n", strcmp("C","A"));
15 fprintf(fi, "Sánchez-Sanchez: %d\n", strcmp("Sánchez", "Sanchez"));
16 fprintf(fi, "Spabcy-Spabzl: %d\n", strcmp("Spabcy", "Spabzl"));
17
18 fclose(fi);
19
20 return 0;
21 }
La salida sería:
A-A: 0
A-B: -1
B-A: 1
C-A: 1
Sánchez-Sanchez: 1
Spabcy-Spabzl: -1
Realmente calcula la diferencia de los códigos ascii de cada uno de los caracteres que constituyen las respectivas
cadenas. Si el resultado es positivo la salida es 1 y las cadenas no están ordenadas alfabéticamente, si es −1 las
cadenas están ordenadas alfabéticamente y si es 0 quiere decir que todos los códigos ASCII son iguales y por tanto,
las cadenas son idénticas.
Función strcpy
Su expresión es strcpy(cadena1, cadena2): copia en la primera cadena de caracteres, el contenido de
la segunda. De manera análoga a lo que ocurría con la función strcat, el tamaño de la cadena a la que apunta:
cadena1, deberá tener un tamaño suficiente para guardar la cadena2. Veamos un ejemplo de esta función,
utilizándola para ordenar una serie de cadenas de caracteres.
J. Sanguino
122 Capítulo – 5. Datos estructurados
6 #define FILA 5
7 #define COLUM 10
8 int main(void)
9 {
10 int i,j;
11
J. Sanguino
5.4 – Matrices 123
5.4. Matrices
Las matrices tienen unas características análogas a las ya vistas para los vectores. Sin embargo, ahora hay que
tener en cuenta que son estructuras con dos índices. El aspecto general de la declaración de una matriz, toma la
forma:
Como ocurre con los vectores, tanto las filas como las columnas, se numeran también a partir de 0. La forma de
acceder a los elementos de la matriz es utilizando su nombre, seguido de las expresiones enteras correspondientes
a los índices, entre corchetes. Esto es, a[i][j] indica el elemento situado en la fila i y columna j.
Es importante destacar que en C, las matrices se almacenan por filas. Esto quiere decir, que los elementos
de una fila ocupan posiciones de memoria contiguas4 . Por tanto, en C, suele ser mucho más eficiente variar
primeramente, el índice de la columna y a continuación el de la fila, si quiere desplazarse por las componentes de
una matriz. Realmente C, considera las matrices, como vectores cuyas componentes son a su vez vectores.
Las componentes de las matrices, al igual que sucedía con los vectores, pueden ser números o caracteres. Un
ejemplo de este último caso, puede apreciarse en el código 5.10 (pág. 122) en la variable apellidos.
La inicialización de matrices sería análoga a la de los vectores. Se pueden inicializar todas sus componentes o
alguna de ellas, poniendo a 0, el resto. Un ejemplo muy simple se muestra en el código 5.12, en el que se inicializa
algunas de las componentes de una matriz y después se imprime en un fichero.
4
No es siempre así en todos los lenguajes. Por ejemplo en FORTRAN y MATLAB las matrices se almacenan por columnas.
J. Sanguino
124 Capítulo – 5. Datos estructurados
Coeficientes de Mat1
-5 0 0
0 0 -6
0 0 0
Coeficientes de Mat2
1 4 0
-7 2 0
A = [ai, j ] con i = 1, . . . , m y j = 1, . . . , n
B = [bi, j ] con i = 1, . . . , m y j = 1, . . . , n
La suma de estas dos matrices es una nueva matriz C ∈ Mm×n definida de la forma
J. Sanguino
5.4 – Matrices 125
Como se observa, se hace variar para cada fila, todas sus columnas.
B = [bk, j ] con k = 1, . . . , p y j = 1, . . . , m
El producto de estas dos matrices es una nueva matriz C ∈ Mn×m definida de la forma
p
X
ci, j = ai, k · bk, j con i = 1, . . . , n; j = 1, . . . , m (5.3)
k=1
Un ejemplo de código que permite el producto de matrices, tal y como se ha establecido el algoritmo (5.3),
puede verse en el programa 5.13. Primeramente se inicializan las matrices y se escriben en un fichero, para com-
probar la entrada de datos. Seguidamente se realiza el producto de matrices (LÍNEAS 44-50), para terminar con la
impresión de la matriz producto.
7 int main(void)
8 {
9 int i, j, k;
10
11 int MatA[N][P] = {{1, 2}, {3, 4}, {5, 6}};
12 int MatB[P][M] = {{-4, 3, -7}, {-1, 11, 2}};
13 int MatC[N][M] = {0};
14
15 FILE *fi;
16
17 fi = fopen("Datos.dat", "w");
18
19 fprintf(fi,"Matriz A: \n");
20 for (i = 0; i < N; i++){
21 for(k = 0; k < P; k++){
22 fprintf(fi, " %4i", MatA[i][k]);
23 }
24 fprintf(fi, "\n");
25 }
26
27 fprintf(fi, "Matriz B: \n");
28 for (k = 0; k < P; k++){
J. Sanguino
126 Capítulo – 5. Datos estructurados
43 //Producto de matrices:
44 for (i = 0; i < N; i++){
45 for(j = 0; j < M; j++){
46 for(MatC[i][j] = 0, k = 0; k < P; k++){
47 MatC[i][j] += MatA[i][k] * MatB[k][j];
48 }
49 }
50 }
51
52 //Salida de resultados
53 fprintf(fi,"\n ********* RESULTADO ******* \n\n");
54 fprintf(fi, "Matriz Producto C:\n");
55 for(i = 0; i < N; i++){
56 for(j = 0; j < M; j++){
57 fprintf(fi, " %4i", MatC[i][j]);
58 }
59 fprintf(fi, "\n");
60 }
61
62 fclose(fi);
63
64 return 0;
65 }
Matriz A:
1 2
3 4
5 6
Matriz B:
-4 3 -7
-1 11 2
Matriz producto C, inicial:
0 0 0
0 0 0
0 0 0
Matriz Producto C:
-6 25 -3
-16 53 -13
-26 81 -23
J. Sanguino
5.5 – Estructuras 127
5.5. Estructuras
Hasta ahora las estructuras de datos utilizadas tenían la particularidad de ser homogéneas (todos las variables
eran del mismo tipo). Con el nombre de estructura se hace referencia a una colección de variables, no necesaria-
mente todas del mismo tipo, agrupadas bajo un mismo identificador o nombre. Se trata entonces, de una estructura
de datos heterogénea.
En el lenguaje C las variables tipo estructura se declaran mediante la palabra struct. El modelo o patrón
para definir estas estructuras tiene el siguiente aspecto:
struct nombre_estructura {
declaración var_1;
declaración var_2;
...
...
declaración var_n;
};
Supongamos que se quiere hacer una lista de los alumnos de la asignatura de Informática. Se define una estructura
llamada alumno. En ella se declaran las variables: Apellido, Nombre, N_mat, Notas y Nota_f. Debido a
que son variables con diferente propósito, serán variables de diferente tipo. Veamos el ejemplo:
struct alumno {
int N_mat;
char Apellido[35];
char Nombre[15];
float Notas[4];
float Nota_f;
};
Cada una de estas variables recibe el nombre de miembro de la estructura. Este código crea o define el tipo de dato
alumno. Para poder declarar las variables de tipo alumno, en C debe utilizarse el identificador o nombre de las
variables, junto con las palabras struct y alumno. Ejemplo:
Estas dos variables estudiante1 y estudiante2, son cada una de ellas una estructura que agrupan una
variable entera: N_mat; dos cadenas de caracteres: Apellido de hasta 35 caracteres y Nombre de hasta 15
caracteres, un vector real de 4 componentes Notas[4] y un número real Nota_f, respectivamente. También se
podían haberse declarado ambas variables, al mismo tiempo que la definición de la estructura. Por ejemplo, en el
caso:
struct alumno {
int N_mat;
char Apellidos[35];
char Nombre[15];
} estudiante1, estudiante2;
Para acceder a los miembros de una estructura se utiliza el operador punto: . , precedido por el identificador
de la estructura y seguido del nombre del miembro. Por ejemplo, para asignar el número de matrícula 12.234 al
primer estudiante, se procedería de la forma:
estudiante1.N_mat = 12234;
J. Sanguino
128 Capítulo – 5. Datos estructurados
y análogamente, el nombre:
estudiante1.Nombre = "Rosendo";
La lectura se hace de forma análoga a la realizada para las variables. Por ejemplo:
o bien
gets(estudiante1.Apellidos);
J. Sanguino
5.5 – Estructuras 129
En este caso, otro_alumno es una estructura de tipo alumno y Grupo_Tarde[90] es un vector de estructuras
en la que cada componente es una estructura de tipo alumno. El miembro Apellidos del alumno 32 vendría
representado de la forma:
Grupo_Tarde[32].Apellidos;
Las estructuras permiten ciertas operaciones globales que por su propia naturaleza no se pueden realizar con los
vectores y matrices, en general con los arrays. Por ejemplo:
Grupo_Tarde[28] = otro_alumno;
hace que se copien todos los miembros de la estructura otro_alumno en los miembros correspondientes de la
estructura Grupo_Tarde[28]. Esta asignación no es posible con los datos estructurados arrays.
Es posible además, que los miembros de una estructura sea a su vez estructuras, previamente definidas. Por
ejemplo, se considera el programa 5.15, que define una estructura anidada, que a su vez se asocia a un vector. Se
trata por tanto, de una muestra de manejo de una estructura anidada junto con un vector de estructuras.
19
20 FILE *fi;
21 fi = fopen("Datos.dat", "w");
22
J. Sanguino
130 Capítulo – 5. Datos estructurados
La salida es:
* Mat: 21345
* Apellidos: Mestre Amor
* Nombre: Roberto
* Nota final: 6.800000
* Mat: 43215
* Apellidos: Beter Patro
* Nombre: Mar
* Nota final: 4.200000
* Mat: 32569
* Apellidos: Santos Puertas
* Nombre: Luis
* Nota final: 7.950000
5.6. typedef
La palabra typedef permite renombrar los tipos con un nombre arbitrario, escogido por el usuario. Supon-
gamos que se desea que real sustituya a float. En este caso se haría:
J. Sanguino
5.6 – typedef 131
Todas estas variables serían ahora del tipo estructura alumno, definida anteriormente.
Este conjunto de pares ordenados con estas operaciones tienen unas propiedades, que se resumen diciendo que tiene
estructura algebraica de cuerpo y recibe el nombre de cuerpo de los números complejos C. Así pues, cualquier
elemento de este conjunto se representa como z = (a, b) ∈ C.
A partir de esta definición se realizan las identificaciones: (x, 0) ≡ x ∈ R y (0, 1) ≡ i. Con lo cual, aplicando
estas identificaciones, junto con la definición de producto se tiene:
que es la llamada forma binómica de un complejo z. Además por definición, si z = x + iy se utiliza la siguiente
nomenclatura
Re (z) = x ∈ R Parte real Im (z) = y ∈ R Parte imaginaria
Operaciones básicas con números complejos son:
z1 + z2 = (x1 + x2 ) + (y1 + y2 )i
z1 · z2 = (x1 x2 − y1 y2 ) + (x1 y2 + x2 y1 )i
J. Sanguino
132 Capítulo – 5. Datos estructurados
Estructura complejo
El concepto de número complejo, se puede abordar en C a partir del concepto de estructura. Un número
complejo puede definirse a través de una estructura con dos miembros: uno correspondiente a la parte real y
otro a la parte imaginaria. Si además se utiliza la instrucción typedef una definición del tipo complejo podría
establecerse de la forma:
struct estruct_complejo {
double real;
double imag;
};
typedef struct estruct_complejo complejo;
o bien de la manera:
typedef struct {
double real;
double imag;
} complejo;
Un ejemplo de utilización de este tipo de estructura puede verse en el código 5.16, en el que declaran dos va-
riables complejas: z y z1 , una de las cuales está inicializada y los valores de la otra se introduce por teclado. A
continuación, calcula la distancia entre ambos y la salida la escribe en el fichero Datos.dat.
J. Sanguino
5.6 – typedef 133
30 z.real, z.imag);
31 fprintf(fi, "\n El numero complejo z es %.4f + %.4f i\n",
32 z1.real, z1.imag);
33 /**********************/
34
35 dist = sqrt((z.real - z1.real) * (z.real - z1.real) +
36 (z.imag - z1.imag) * (z.imag - z1.imag));
37
Estructura complex
En las últimas actualizaciones realizadas en el lenguaje C, ya está disponible un tipo de variable compleja, que
se indentifica con el nombre de complex. Es la misma idea de estructura definida en el código 5.16, sin embargo
esta modalidad de tipo que ofrece C, tiene más posibilidades y es más completa5 . Es posible operar con este tipo de
variables directamente, sin necesidad de hacer referencia a los miembros de la estructura, como se hizo en el caso
anterior. Para poder utilizar este tipo, es necesario añadir en el preámbulo del programa la directiva #include
<complex.h>. En el código 5.17, se muestra un ejemplo de utilización de este tipo para realizar una serie de
operaciones elementales con números complejos.
17 fi = fopen("Datos.dat", "w");
18
19 fprintf(fi, "Números complejos introducidos:\n");
20 fprintf(fi, "z1 = %.2f %+.2fi\n", creal(z1), cimag(z1));
21 fprintf(fi, "z2 = %.2f %+.2fi\n", creal(z2), cimag(z2));
22
23 suma = z1 + z2;
24 fprintf(fi, "La suma z1 + z2 = %.2f %+ .2fi\n",
25 creal(suma), cimag(suma));
26
27 prod = z1 * z2;
28 fprintf(fi, "El producto z1 * z2 = %.2f %+.2fi\n",
29 creal(prod), cimag(prod));
5
En el entorno D EV-C++ que se utiliza en este curso, el compilador ya ofrece este tipo complex.
J. Sanguino
134 Capítulo – 5. Datos estructurados
30
31 cociente = z1 / z2;
32 fprintf(fi, "El cociente z1/z2 = %.2f %+.2fi \n",
33 creal(cociente), cimag(cociente));
34
35 conjugado = conj(z1);
36 fprintf(fi, "El conjugado de z1 = %.2f %+.2fi \n",
37 creal(conjugado) ,cimag(conjugado));
38
39 potencia = cpow(z1,2 - 0.5*I);
40 fprintf(fi, "z1 elevado a 2-0.5i = %.4f %+.4fi \n",
41 creal(potencia), cimag(potencia));
42
43 fclose(fi);
44
45 return 0;
46 }
En las LÍNEAS 9-10 se aprecia cómo debe introducirse una constante compleja. Es importante, que la i de la parte
imaginaria de la expresión binómica, se representa en C con I. Esto es, el número complejo: 2 − 21 i se expresa
como 2 - 0.5*I. Por último resaltar, que al aplicar la función pow de la biblioteca, al ser utilizada con el tipo
complejo debe ponerse como cpow (véase LÍNEA 39).
6
El compilador que trae el entorno D EV–C++, que se utiliza en este curso, permite esta posibilidad de dimensionamiento dinámico.
J. Sanguino
5.7 – Dimensionamiento dinámico (Variable Length Arrays – VLA) 135
5 int main(void)
6 {
7
8
9 int N;
10 unsigned int ind;
11
12 FILE *fi;
13
14 fi = fopen("Datos.dat", "w");
15
16 printf("\n\n **** Este programa determina la tabla de los número de Fibonacci
****\n\n");
17 printf("Introduce un entero 2 < N -> ");
18 while(scanf(" %i", &N) != 1 || N < 3 ){
19 while(getchar() != ’\n’); /* limpia el flujo de entrada*/
20 /*Se repite la entrada */
21 printf("\n\nHas de introducir entero positivo 2 < N -> ");
22 }
23 while(getchar() != ’\n’);
24
25
26 // Dimensionamiento dinámico
27 unsigned long long TablaFibo[N];
28
29 //Inicialización
30 TablaFibo[0] = 0ULL;
31 TablaFibo[1] = 1ULL;
32
33 // Construye los términos de la sucesión
34 for(ind = 2; ind < N; ind++){
35 TablaFibo[ind] = TablaFibo[ind-2] + TablaFibo[ind-1];
36 }
37
38 /* Imprime la tabla */
39 fprintf(fi, "Los %i primeros números de Fibonacci son: \n\n", N);
40 for (ind = 0; ind < N; ind++){
41 fprintf(fi, " %llu ; ", TablaFibo[ind]);
42 }
43
44 fclose(fi);
45
46 return 0;
47
48 }
En la LÍNEA 27 se genera el dimensionamiento dinámico. El tamaño del vector TablaFibo se adapta a la cantidad
de números de la sucesión de Fibonacci que se desean. Así pues, como ya no existe la limitación inicial de 30
componentes, como consecuencia del dimensionamiento fijo del código 5.4, se ha eliminado esta restricción de la
entrada de datos. Por otra parte, es importante señalar, cómo se realiza la inicialización. Con el dimensionamiento
dinámico ya no se permiten inicializaciones del tipo
// Dimensionamiento dinámico
unsigned long long TablaFibo[N] = {0ULL, 1ULL};
similar a la que se hacía en la LÍNEA 10 del código 5.4 (pág. 115). Ahora la inicialización se debe hacer según se
J. Sanguino
136 Capítulo – 5. Datos estructurados
Método de la burbuja
El vector ordenado es
v[1] v[2]....v[n]
El método de la burbuja puede modificarse para seguir un procedimiento más coherente, aunque no aumenta
la eficiencia y por tanto, no tiene ningún efecto sobre el orden del algoritmo, que seguirá siendo proporcional a n2
(véase Wirth [20, pág. 71 y 72])
J. Sanguino
5.8 – Algoritmos de ordenación 137
i <- 1
repetir:
cambio <- 1
desde j <- 1 hasta (n-1)-i realiza
si (v[j] > v[j+1]) entonces
temp <- v[j]
v[j] <- v[j+1]
v[j+1] <- temp
cambio <- 0
fin-si
fin-desde
i <- i + 1
hasta_que (cambio > 0)
El vector ordenado es
v[1] v[2]....v[n]
El vector ordenado es
v[1] v[2]....v[n]
J. Sanguino
138 Capítulo – 5. Datos estructurados
El vector ordenado es
v[1] v[2]....v[n]
Método de Shell
El vector ordenado es
v[1] v[2]....v[n]
5.9. Problemas
1. Dado un vector real de doble precisión:
x = (x1 , x2 , . . . , xn )
J. Sanguino
5.9 – Problemas 139
2. Escribe un programa que abra un fichero con un nombre especificado por el usuario y añada lo que el usuario
teclea.
3. Dada una cadena de caracteres escribe un programa que cuente el número de caracteres e invierta la frase
(debe aparecer escrita al revés). Una ejecución típica del programa debe ser:
El polinomio producto
J. Sanguino
140 Capítulo – 5. Datos estructurados
Se pide que hagas un programa que calcule el producto de dos polinomios cuyos coeficientes se introducirán
por el teclado y el polinomio producto resultante, se imprimirá en un fichero de datos, cuyo nombre se deberá
introducir por teclado, previamente.
5. Realiza una tabla de números primos. Se debe introducir un número natural n y escribir en un fichero, la
tabla de todos los primos menores o iguales que n, así como la cantidad de números primos que hay. Un
ejemplo de ejecución sería:
y el resultado
1 ; 2 ; 3 ; 5 ; 7 ; 11 ; 13 ; 17 ; 19 ; 23
6. Dada una matriz A ∈ Mm×n haz un programa que imprima su matriz transpuesta en un fichero de datos. A
continuación, si A es una matriz cuadrada de dimensión n, haz un programa que determine su transpuesta
y la almacene en las mismas posiciones de memoria que la matriz de entrada (obviamente los datos de
la matriz de entrada cambiarán por los coeficientes de la matriz transpuesta). Si quieres puedes utilizar el
dimensionamiento dinámico.
En un espacio vectorial euclídeo (como (Rn , ·)), esta operación se llama producto diádico o tensorial de a
y b y da lugar a una forma bilineal que recibe el nombre de tensor y opera de la forma
(a ⊗ b) · v = (b · v) a ∀ v ∈ Rn
J. Sanguino
5.9 – Problemas 141
9. Escribe un programa que determine si una matriz cuadrada es diagonal dominante. Se dice que una matriz
cuadrada es diagonal dominante si
n
X
|ai, i | > |ai, j |
j=1
i6=j
10. Se pide que realices un programa que permita descomponer una matriz cuadrada real de tamaño n, A en
suma de una matriz simétrica S y una matriz antisimétrica T, utilizando la siguiente expresión:
1 1
A + At + A − At
A=
|2 {z } |2 {z }
S T
en donde At es la matriz traspuesta de A. Una ejecución típica del programa debe ser:
Introduce A[1][1] = 1
Introduce A[1][2] = 2
Introduce A[1][3] = 3
Introduce A[2][1] = 4
Introduce A[2][2] = 5
Introduce A[2][3] = 6
Introduce A[3][1] = 7
Introduce A[3][2] = 8
Introduce A[3][3] = 9
11. Resuelve la ecuación de segundo grado utilizando el tipo complex, para la solución.
J. Sanguino
142 Capítulo – 5. Datos estructurados
12. Haz un programa que pida un número complejo por teclado y compruebe la igualdad:
Introduce X[2] = 2
Introduce Y[2] = 1
Introduce X[3] = 3
Introduce Y[3] = 4
Introduce X[4] = 0
Introduce Y[4] = 3
Introduce X[5] = 1
Introduce Y[5] = 2
El perímetro es 12.21090
J. Sanguino
CAPÍTULO 6
Punteros
6.1. Introducción
Los punteros son variables que almacenan direcciones de memoria. Esto permite desarrollar programas a bajo
nivel que pueden resultar más flexibles y eficientes. Sin embargo, tiene el inconveniente de hacer también a los
programas muy susceptibles, de generar errores de ejecución, si estas variables no se operan correctamente.
En este capítulo se tratará principalmente con punteros a enteros como ejemplo, pero todos los resultados y
planteamientos pueden generalizarse de manera inmediata a puntero de cualquier tipo, con sólo intercambiar el
nombre.
Para trabajar con las direcciones de memoria, hay dos operadores básicos
Operador dirección (&). Dada una variable es posible conocer su dirección mediante el operador & . Si entero
es una variable entera con cierto valor, entonces mediante la expresión
&entero;
Operador indirección (*). Si ptr es una variable declarada como puntero a entero entonces esta variable sólo
almacena una dirección. Si se ha depositado un valor en esta dirección, es posible saber cuál es éste, mediante
la expresión
*ptr;
143
144 Capítulo – 6. Punteros
En este caso, el asterisco * tiene una interpretación distinta al utilizado en la declaración de la variable.
En este caso, se declara un puntero a entero (LÍNEA 6)1 y almacena la dirección de la variable prueba (LÍNEA
8). Así pues, *ptr: almacena el valor de prueba; ptr: almacena la dirección de prueba y &ptr: muestra la
dirección de la variable ptr, que al fin y al cabo, es otra variable y por tanto, C le asigna una dirección. La salida
en pantalla se muestra a continuación:
Una representación gráfica de la salida del programa 6.1 puede verse en la figura 6.1. Una variable puntero,
Variables
2293312 2293324
&ptr &prueba
(cualquiera que sea el tipo a la que esté asociada) se suele identificar con el formato %p. Sin embargo, esta opción
hace que la dirección de memoria se exprese en hexadecimal. En este curso, para que resulte más clara la dirección
de memoria, se utilizará el formato %u (asociado a un tipo unsigned), que permite su expresión en forma decimal.
Para declarar una variable como puntero se debe conocer el tipo de variable a la que está apuntando dicho
puntero. La razón es que las variables de distintos tipos ocupan diferentes cantidades de memoria y existen opera-
ciones con punteros que requieren conocer el tamaño de almacenamiento. A continuación se muestra un ejemplo
con diferentes tipos de punteros
J. Sanguino
6.2 – Variables y punteros 145
5
6 char *ptrchar; //*** Declaración puntero
7
8 int entero, cambio = 9;
9 int *ptr, *otroptr; //*** Declaración puntero
10
11 float real;
12 float *ptrfloat; //*** Declaración puntero
13
14 double realargo;
15 double *ptrdouble; //*** Declaración puntero
16
17
18 entero = 7;
19 ptr = &entero;
20 printf("entero -> %i, direccion -> %u <-> %p (hex.)\n", entero, ptr, ptr);
21
22 *ptr = 14;
23 cambio = *ptr;
24 printf("entero -> %i; direccion -> %u; ", entero, ptr);
25 printf("cambio -> %i \n", cambio);
26
27 otroptr = ptr;
28 *otroptr = 106; /*cambia el valor de la v. entero*/
29 printf("otroptr -> %u; ptr -> %u; \n", otroptr, ptr);
30 printf("entero -> %i; \n\n", entero);
31
32 real = 2.595;
33 realargo = 246.8932;
34
35 ptrfloat = ℜ
36 ptrdouble = &realargo;
37 printf("real -> %f; ptrfloat -> %u; \n", real, ptrfloat);
38 printf("realargo -> %f; ptrdouble -> %u; ", realargo, ptrdouble);
39
40 printf("Tamaño de ptrchar -> %u; bytes\n", sizeof(char *));
41 printf("Tamaño de ptr -> %u bytes\n", sizeof(int *));
42 printf("Tamaño de ptrfloat -> %u bytes\n", sizeof(float *));
43 printf("Tamaño de ptrdouble -> %u bytes\n", sizeof(double *));
44
45
46 return 0;
47 }
Es interesante destacar cómo en las LÍNEAS 6, 9, 12 y 15 aparecen las declaraciones de variables puntero a char,
int, float y double respectivamente. Además, en la LÍNEA 19 mediante el operador & se introduce la direc-
ción de la variable entero, en el puntero ptr. A continuación, en la LÍNEA 22 se utiliza el operador indirección
para cambiar el valor de la variable entero al valor 14 y que posteriormente, se introduce en la variable cambio,
J. Sanguino
146 Capítulo – 6. Punteros
cambio = entero;
En las LÍNEAS 27-28 se utiliza un nuevo puntero, para cambiar el valor de la variable entero a 106. En la última
parte del programa 6.2 se ha obtenido el tamaño de las variables puntero. Todas ellas, ocupan 8 bytes2 .
ptr = ptr + 1;
ptr = ptr - 2;
ptr = ptr + contador;
ptr++;
ptr--;
La salida en pantalla es
2
Este resultado depende del compilador y del sistema operativo. En este caso, se utiliza el entorno D EV–C++ 5.11, cuyo compilador
está configurado para trabajar a 64 bits. Por esta razón aparece un tamaño de 8 bytes en la salida.
3
Hay que tener en cuenta, que el compilador no controla que con la ejecución de estas operaciones se pueda exceder de la cantidad de
memoria direccionable o que la variable tome valores negativos, lo cual lleva irremediablemente a un error de ejecución.
J. Sanguino
6.3 – Vectores y punteros 147
ptr_c -> 2293287; prt_i -> 2293280; ptr_f -> 2293276; prt_double -> 2293264
ptr_c-- -> 2293286; prt_i++ -> 2293284; ptr_f++ -> 2293280; prt_double++ -> 2293272
ptr_c+3 -> 2293289; prt_i-5 -> 2293264; ptr_f+2 -> 2293288; prt_double-2 -> 2293256
Merece la pena analizar el resultado obtenido. En la primera fila de la salida, cada puntero tiene almacenada la
dirección de sus respectivas variables declaradas, por medio de las sentencias de las LÍNEAS 6-9 del código 6.3.
Como ya se ha comentado (véase subsección 1.3.3 en la pág. 6), la memoria del ordenador se agrupa en bytes. Los
tamaños de estos grupos de bytes, depende del tipo de variable declarada. Por tanto, los números que se muestran
en la primera línea de la salida, corresponden a la dirección del primer byte, del grupo de bytes que tiene asignado
cada tipo de variable. De esta forma, la variable c, como es del tipo carácter y únicamente ocupa un byte, el número
2293287 es la dirección de ese byte. Sin embargo, en el caso de puntero a entero: ptr_i, el número que muestra
en la salida: 2293280 corresponde al primer byte de los cuatro que tiene asignados por ser una dirección a una
variable entera de 4 bytes. Algo análogo sucede con el puntero ptr_f, que almacena la dirección del primer byte
de los cuatro asignados. En ptr_double se indica la dirección del primer byte de los ocho que tiene establecidos
por ser un puntero a double.
Esta disposición, afecta a las operaciones aritméticas: cuando en la LÍNEA 15 se decrementa e incrementa estas
variables, resulta que ptr_c apunta al byte anterior (por ocupar la variable un sólo byte, pero al incrementar
ptr_i en una unidad, su valor aumenta en 4 bytes; y correspondería a la posición asignada al siguiente entero.
Lo análogo sucede con la variable prt_f. En el caso de ptr_double, al ser un puntero a double el incremento
en una unidad en la variable, supone un aumento de ocho bytes para posicionarlo en la dirección de la siguiente
variable double. Con las sentencias de la LÍNEA 19, ptr_c se incrementa en 3 unidades, con lo que pasa a señalar
la dirección del byte 2293573; ptr_i se decrementa en 5 unidades, con lo cual su dirección pasa a señalar 20
bytes anteriores (20 = 4 bytes × 5); algo similar sucede con ptr_f. Por último ptr_double se decrementa en
2 unidades, lo que implica que su dirección disminuye en 16 bytes (16 = 8 bytes × 2).
Mediante la asignación
ptr = &Vtr[0]
no sólo el puntero ptr almacena la dirección de la variable Vtr[0], sino que automáticamente, ptr+1 es la
dirección de la variable Vtr[1] (es decir que ptr+1 coincide con &Vtr[1]); ptr+2 almacena la dirección
de Vtr[2] y así sucesivamente hasta ptr+9, que almacena la dirección de Vtr[9]. Además, por la propia
definición de puntero, se cumplen las igualdades (aritméticas)
*(ptr + i) = Vtr[i]
con i = 0, . . . , 9
A continuación se muestra un sencillo programa en donde se aprecia esta equivalencia.
J. Sanguino
148 Capítulo – 6. Punteros
En este programa se declara e inicializa un vector de tipo int (LÍNEA 7) y se escribe su contenido de dos maneras:
una es utilizando la estructura habitual de vector (LÍNEAS 13-15) y la otra mediante un puntero (LÍNEAS 23-25). En
la LÍNEA 8 se declara la variable puntero a entero: ptr y en la LÍNEA 19 se introduce la dirección de Vtr, en esta
variable. El resultado es el siguiente:
Existe todavía una relación más profunda entre los vectores y los punteros: el nombre o identificador de un
vector es un puntero a la dirección de memoria que contiene el primer elemento del vector (realmente es un
puntero a constante y siendo esta constante el nombre o identificador del vector). Siguiendo con el ejemplo anterior,
resulta que Vtr contiene un valor que coincide con &Vtr[0]. Esto es, Vtr almacena la dirección de la primera
componente del vector. Esta característica puede extenderse:
Puesto que el nombre de un vector es un puntero a constante, cumplirá las normas de la aritmética de
punteros (véase subsección 6.2.1 en la pág. 146). Por tanto, si Vtr contiene la dirección de Vtr[0],
entonces (Vtr+1) contendrá la dirección de Vtr[1] y así sucesivamente hasta (Vtr+n), que contendrá
la de Vtr[n].
Por otra parte, a los punteros se les puede asignar índices, al igual que a los vectores. De esta manera, si
J. Sanguino
6.3 – Vectores y punteros 149
ptr a apunta a Vtr[0], se admite escribir ptr[i], siendo equivalente a Vtr[i]. Esto es, se tienen las
igualdades (en sentido aritmético)
con i = 0, . . . , 9.
ptr = &Vtr[0]
LLegado este punto, es importante hacer la siguiente observación: al ser el identificador Vtr una constante,
se cumple la igualdad (en sentido puramente aritmético):
Se tratan de constantes que almacenan la misma dirección de memoria, pero aunque C les asocia el mismo valor,
&Vtr es conceptualmente distinto a Vtr y &Vtr[0]. Una representación gráfica de estas relaciones entre puntero
y vector puede verse en la figura 6.2.
2293296 ptr
2293288 &ptr
En este gráfico se muestra que &Vtr no se puede asignar al puntero ptr (no sale ninguna flecha de la casilla
&Vtr con dirección a la casilla de ptr), aunque almacena la misma dirección de memoria que Vtr y &Vtr[0].
Las direcciones de memoria se ajustan a las obtenidas como resultado del código 6.4 anterior.
En en el siguiente programa se muestran las diferentes equivalencias a la hora de tratar a un vector y un puntero,
obteniéndose el mismo resultado, ya sea expresado a través de un vector o mediante el puntero asociado,
J. Sanguino
150 Capítulo – 6. Punteros
12
13 for (i = 0; i < DIM; i++)
14 fprintf(fi, "Vtr[ %i] = %2i\n", i, Vtr[i] );
15 fprintf(fi, "\n");
16
17 for (i = 0; i < DIM; i++)
18 fprintf(fi, "*(Vtr + %i) = %2i\n", i, *(Vtr + i));
19 fprintf(fi, "\n");
20
21 for (ptr = &Vtr[0], i = 0; i < DIM; i++)
22 fprintf(fi, "*(ptr + %i) = %2i \n", i, *(ptr + i) );
23 fprintf(fi, "\n");
24
25 for (ptr = &Vtr[0], i = 0; i < DIM; i++)
26 fprintf(fi, "ptr[ %i] = %2i\n", i, ptr[i]);
27
28 fclose(fi);
29
30 return 0;
31 }
La salida se almacena en el fichero Datos.dat y para todos los procedimientos el resultado es el mismo:
Vtr[0] = 28
Vtr[1] = 29
Vtr[2] = 30
Vtr[3] = 31
*(Vtr + 0) = 28
*(Vtr + 1) = 29
*(Vtr + 2) = 30
*(Vtr + 3) = 31
*(ptr + 0) = 28
*(ptr + 1) = 29
*(ptr + 2) = 30
*(ptr + 3) = 31
ptr[0] = 28
ptr[1] = 29
ptr[2] = 30
ptr[3] = 31
Es importante destacar que en el for de la LÍNEA 25, se ha tenido que inicializar de nuevo la variable ptr, ya que
su valor fue alterado durante la sentencia for de la LÍNEA 21.
1. aux = *ptr.
2. ptr = ptr + 1.
J. Sanguino
6.3 – Vectores y punteros 151
4 int main(void)
5 {
6 int i, aux;
7 int vector[DIM] = {28, 29, 30};
8 int *ptr;
9
10 FILE *fi;
11 fi = fopen("Datos.dat", "w");
12
13 ptr = &vector[0];
14 fprintf(fi, " Inicial %u <-> %2d;\n\n ", ptr, *ptr);
15 for (i = 0; i <= DIM-1; i++){
16 fprintf(fi, "ptr = %u <-> ", ptr);
17 aux = *ptr++;
18 fprintf(fi, "*ptr++ = %2d; ", aux);
19 }
20 fprintf(fi, "\n\n");
21
22 fclose(fi);
23 return 0;
24 }
ptr = 2293280 <-> *ptr++ = 28; ptr = 2293284 <-> *ptr++ = 29; ptr = 2293288 <-> *ptr++ = 30;
1. ptr = ptr + 1.
2. aux = *ptr.
J. Sanguino
152 Capítulo – 6. Punteros
24 }
ptr = 2293280 <-> *++ptr = 29; ptr = 2293284 <-> *++ptr = 30; ptr = 2293288 <-> *++ptr = 0;
En este caso, el programa muestra el contenido de la siguiente dirección de memoria. En el último, como
sobrepasa el tamaño del vector, muestra un contenido, posiblemente anterior.
1. aux = *ptr.
2. *ptr = *ptr + 1.
ptr = 2293280 <-> (*ptr)++ = 28; ptr = 2293280 <-> (*ptr)++ = 29; ptr = 2293280 <-> (*ptr)++ =
30;
En este caso, el programa no cambia el valor de ptr, únicamente el valor que contiene. Así pues, queda
modificado el valor de vector[0], que resulta ser ahora 30.
1. *ptr = *ptr + 1.
2. aux = *ptr.
J. Sanguino
6.3 – Vectores y punteros 153
En esta situación, es interesante aplicar esta notación con la del ejemplo anterior, para observar su resultado.
Para ello se propone el siguiente programa:
22 ptr = &vector[0];
23 fprintf(fi, " Inicial %u <-> %2d;\n\n ", ptr, *ptr);
24 for (i = 0; i <= DIM-1; i++){
25 fprintf(fi, "ptr = %u <-> ", ptr);
26 aux = ++(*ptr);
27 fprintf(fi, "++*ptr = %2d; ", aux);
28 }
29 fprintf(fi, "\n\n");
30
31
32 fclose(fi);
33 return 0;
34 }
De nuevo, no cambia el valor de ptr. Sin embargo, en la LÍNEA 5 de la salida muestra un valor para *ptr
de 31. Ello es debido a posición sufija del operador incremento en la LÍNEA 17 del código 6.9, que una vez
que imprime el valor de 30, se incrementa en una unidad, con lo cual en la LÍNEA 22 del mismo programa,
el valor de vector[0] y por tanto, el de *ptr es 31.
J. Sanguino
154 Capítulo – 6. Punteros
Como consecuencia, mientras que es crucial conocer el número total de columnas de la matriz, ya que junto con
los índices, permite la localización del elemento sobre este vector, saber el número total de filas, resulta accesorio.
Ello conduce a definir un puntero a una matriz de tamaño igual al número de columnas. Veamos esta situación con
un simple ejemplo. Se trata de asociar el vector de enteros
V = [1, 2, 3, 4]
a la matriz PtrV ∈ M1×4 . Esta matriz debe tener por tanto, 4 columnas. Se define entonces el puntero a matriz
de 4 enteros:
int (*PtrV)[4];
que va a configurar la matriz fila. Esta expresión, también puede interpretarse como un puntero a un entero de
tamaño 4. En efecto, V es un vector de cuatro enteros, con lo cual se puede identificar al vector V como una
única variable entera que tiene a su vez, un tamaño de 4 enteros. Realmente es así como C configura las matrices.
Consecuentemente, tiene sentido la asignación:
PtrV = &V;
Obsérvese que dicha asignación no tendría sentido si PtrV se hubiese declarado como un puntero a entero:
int *PtrV;
tal y como se ha comentado en la sección 6.3 (pág. 147). Así pues, ahora no tienen sentido asignaciones del tipo
PtrV = &V[0] o equivalentemente PtrV = V; de hecho da error de compilación. Como consecuencia de este
proceso de asignación, se deducen las siguientes igualdades lógicas (en el sentido aritmético):
por tanto: PtrV[0] + 1 es equivalente a V + 1 y así sucesivamente. O bien, utilizando la notación de índices
se tendría la equivalencia entre PtrV [0][1] y V[1] y así sucesivamente con todas las componentes del vector.
El código del programa que ejecuta la asociación del vector V con la matriz de una fila y 4 columnas es el siguiente:
J. Sanguino
6.4 – Matrices y punteros 155
4 int main(void)
5 {
6 int i, j;
7 int V[COLUM] ={1, 2, 3, 4};
8 int (*PtrV)[COLUM];
9
10
11 FILE *fi;
12 fi = fopen("Datos.dat", "w");
13
14 fprintf(fi, "&V: %u; V: %u\n\n", &V, V);
15 for (i = 0; i < COLUM; i++)
16 fprintf(fi, " V[ %1d] = %7d; ", i, V[i]);
17 fprintf(fi, "\n");
18 for (i = 0; i < COLUM; i++)
19 fprintf(fi, "&V[ %1d] = %u; ", i, &V[i] );
20 fprintf(fi,"\n\n");
21
22 PtrV = &V;
23 fprintf(fi,"Asignación de dirección de memoria: PtrV = &V\n\n");
24 fprintf(fi, "&PtrV: %u; PtrV: %u; PtrV[0]: %u;\n", &PtrV, PtrV, PtrV[0]);
25 for (i = 0; i < COLUM; i++)
26 fprintf(fi, " PtrV[0][ %1d] = %8d; ", i, PtrV[0][i] );
27 fprintf(fi,"\n");
28 for (i = 0; i < COLUM; i++)
29 fprintf(fi, " *(PtrV + %1d) = %9u; ", i, *(PtrV + i) );
30 fprintf(fi, "\n");
31 for (i = 0; i < COLUM; i++)
32 fprintf(fi, " PtrV[0] + %1d = %u; ", i, PtrV[0] + i );
33 fprintf(fi, "\n");
34
35
36 fclose(fi);
37
38 return 0;
39 }
La interpretación gráfica del proceso puede verse en la figura 6.3. Es interesante comparar esta figura con la 6.2 en
la pág. 149. En este caso, debido a la asignación planteada (LÍNEA 22), la flecha, en la figura 6.3, sale de la casilla
&V.
El lenguaje C permite una declaración alternativa a la que se ha utilizado para el puntero PtrV. Se puede
también declarar de la forma
int PtrV[][4];
Tal vez esta sea más intuitiva, en el sentido de al estar vacía la primera componente (número de filas), comunique
J. Sanguino
156 Capítulo – 6. Punteros
2293296 PtrV
2293288 &PtrV
PtrV
2293296 &V
un cierto efecto de variabilidad; mientras que la segunda (número de columnas), que es importante tal y como se
ha comentado anteriormente, se deja fija.
El siguiente paso, consiste en asociar una matriz dada, a un puntero. Se ha explicado anteriormente, que
resulta fundamental conocer el número total de columnas de la matriz. Siguiendo con el planteamiento del ejemplo
anterior, se considera ahora la matriz entera
1 2 3 4
5 6 7 8
Mat =
9 10 11 12
13 14 15 16
La idea es asociarla a un puntero. Para ello se define, de manera análoga al caso anterior, el puntero:
Este puntero señalará el comienzo de cada fila de la matriz. Así pues, en este caso tiene sentido la asignación
PrtMat = &Mat[0];
No debe olvidarse, que C entiende que la matrices son vectores, cuyas componentes son a su vez variables pero
de tamaño coincidente con el número de columnas. De esta manera, Mat[0] es el nombre de la variable que
almacena los valores de la primera fila. Por tanto, Mat[0] representa a una única variable entera que contiene
a su vez, 4 enteros, al igual que sucedía con el vector V del ejemplo anterior y queda justificada la asignación
anterior. Así pues, si
PtrMat = &Mat[0]
entonces como consecuencia, ocurre la igualdad (aritmética)
*PtrMat = Mat[0]
Teniendo en cuenta las identificaciones entre vectores y punteros planteadas anteriormente (véase sección 6.3,
pág. 147), entonces se dan las siguientes igualdades lógicas (en el sentido aritmético):
J. Sanguino
6.4 – Matrices y punteros 157
con la diferencia que Mat es una constante, mientras que PrtMat es una variable. El resultado del programa 6.11
permite observar estas asignaciones con direcciones de memorias concretas:
J. Sanguino
158 Capítulo – 6. Punteros
PtrMat + 0 = 2293248
PtrMat[0][0]= 1; PtrMat[0][1]= 2; PtrMat[0][2]= 3; PtrMat[0][3]= 4;
PtrMat[0]+0 = 2293248; PtrMat[0]+1 = 2293252; PtrMat[0]+2 = 2293256; PtrMat[0]+3 = 2293260;
PtrMat + 1 = 2293264
PtrMat[1][0]= 5; PtrMat[1][1]= 6; PtrMat[1][2]= 7; PtrMat[1][3]= 8;
PtrMat[1]+0 = 2293264; PtrMat[1]+1 = 2293268; PtrMat[1]+2 = 2293272; PtrMat[1]+3 = 2293276;
PtrMat + 2 = 2293280
PtrMat[2][0]= 9; PtrMat[2][1]= 10; PtrMat[2][2]= 11; PtrMat[2][3]= 12;
PtrMat[2]+0 = 2293280; PtrMat[2]+1 = 2293284; PtrMat[2]+2 = 2293288; PtrMat[2]+3 = 2293292;
PtrMat + 3 = 2293296
PtrMat[3][0]= 13; PtrMat[3][1]= 14; PtrMat[3][2]= 15; PtrMat[3][3]= 16;
PtrMat[3]+0 = 2293296; PtrMat[3]+1 = 2293300; PtrMat[3]+2 = 2293304; PtrMat[3]+3 = 2293308;
Una interpretación gráfica de este planteamiento y con a las direcciones resultantes obtenidas a partir del progra-
ma 6.11, puede verse en la figura 6.4.
2293248 PtrMat
2293240 &PtrMat
Es inmediato, que el caso anterior se puede generalizar a matrices de cualquier tamaño, cambiando el número
J. Sanguino
6.5 – Estructuras y punteros 159
de columnas y de cualquier tipo, sustituyendo int por char, float, double, . . . y de forma respectiva al
puntero en cuestión.
Se puede acceder a los miembros de una estructura a través de un puntero de dos maneras indistintamente:
1. Accediendo a la estructura:
(*Ptr1).N_mat;
Los paréntesis son necesarios porque el operador . tiene una mayor precedencia que la dirección *.
Es un procedimiento más intuitivo que el anterior y por esta razón es más utilizado.
Como ejemplo, se presenta el programa 6.12 que es una modificación del código 5.14, para emplear un puntero a
estructura.
12 FILE *fi;
13 fi = fopen("Datos.dat", "w");
14
15 Ptr = &estudiante;
16 printf("Introduce el Número de matrícula -> ");
17 scanf(" %d", &Ptr -> N_mat);
18 while (getchar() != ’\n’); /* Para vaciar el buffer */
19 printf("\n\n");
20 printf("Introduce los Apellidos -> ");
21 gets(Ptr -> Apellidos);
22 printf("\n\n");
23 printf("Introduce el Nombre -> ");
24 gets(Ptr -> Nombre);
25
26 fprintf(fi, " Los datos introducidos son: \n\n");
27 fprintf(fi, "* Número de matrícula: %d\n", Ptr -> N_mat);
J. Sanguino
160 Capítulo – 6. Punteros
La salida es análoga a la obtenida para el código 5.14, dependiendo de los datos introducidos:
6.7.1. malloc()
Es la manera más habitual de reservar o direccionar bloques de memoria de manera dinámica. La función
genera o asigna un bloque de memoria en bytes, cuyo tamaño es el argumento de la función. La función devuelve
un puntero *void al bloque de memoria asignado, por tanto hay que hacer una conversión al tipo del puntero
requerido. El prototipo de la función es de la forma
int *PtrInt
PtrInt = (int *)malloc( 10 * sizeof(int) );
La función sizeof, que ya fue utilizada anteriormente (véase sección 3.2 en la pág. 27 o bien en el código 6.2,
pág. 144), indica el tamaño en bytes, del tipo int.
J. Sanguino
6.7 – Gestión dinámica 161
En caso que haya problemas en direccionar los bloques de memoria, malloc() devuelve el valor NULL. Es
por tanto, conveniente preguntar después de una operación de asignación de memoria, si todo está correcto.
int *PtrInt
PtrInt = (int *)malloc( 10 * sizeof(int) );
if( PtrInt == NULL ){
printf("No hay memoria disponible\n");
exit(1);
}
La función exit es una función proporcionada por C para manejar posibles errores. No devuelve ningún valor,
pero necesita un argumento numérico. La opción exit(0) indica una salida normal. Sin embargo, cualquier
otro valor distinto de 0, señala una situación anormal. La ventaja de la utilización de esta función es que antes
de salir del programa, cierra todos los ficheros y vacía el buffer. Con lo cual se obtiene una salida ordenada
del programa. El programa 6.13 muestra un ejemplo de direccionamiento dinámico de un vector, cuyo objetivo
consiste en determinar el valor máximo de las componentes del vector
13 FILE *fi;
14 fi = fopen("Datos.dat", "w");
15
16 printf("Introduce la dimensión del vector -> ");
17 scanf(" %d", &Dim);
18 while (getchar() != ’\n’);
19
20 Vect = (float *) malloc( Dim * sizeof(float));
21 if (Vect == NULL ) {
22 printf("Error en la asignación de memoria\n");
23 getchar();
24 exit(1);
25 }
26
27 printf("\n\n Introduce las componentes del vector: \n");
28 for(i = 0; i < Dim; i++){
29 printf("V[ %d] = ", i+1);
30 scanf(" %f", Vect + i); // o bien scanf(" %f", &Vect[i]);
31 while(getchar() != ’\n’);
32 }
33
34 fprintf(fi, "El vector de dimensión %2d introducido es: \n\n", Dim);
35 for (i = 0; i < Dim; i++)
36 fprintf(fi,"Vec[ %1d] = %.4f ; ", i, *(Vect +i));
37 fprintf(fi, "\n\n");
38
39 for(MaxVect = Vect[0], i=1; i < Dim; i++)
40 MaxVect = MAX(MaxVect, Vect[i]);
J. Sanguino
162 Capítulo – 6. Punteros
41
42 fprintf(fi, "El valor máximo es: %.4f\n", MaxVect);
43
44 fclose(fi);
45
46 return 0;
47 }
Al igual que sucede con los vectores, las matrices también pueden direccionarse dinámicamente. Hasta ahora,
las matrices se asociaban con punteros a variables de un determinado tipo, cuyo tamaño coincidía con el número
de columnas de la matriz (véase LÍNEA 8 del código 6.10 (pág. 154) o LÍNEA 9 del código 6.11 (pág. 157)). Sin
embargo, a través de la función malloc() una disposición de datos matricial se puede asociar a un doble puntero,
mediante dos pasos:
1. Se reserva espacio en la memoria para un vector de punteros que señalarán las direcciones de los vectores
fila, mediante la instrucción:
donde MatReal es un doble puntero a double: double **MatReal. Un ejemplo completo se presenta
en el programa 6.14.
J. Sanguino
6.7 – Gestión dinámica 163
28 exit(1);
29 }
30 fprintf(fi, "&MatReal: %u; MatReal: %u; \n\n", &MatReal, MatReal);
31 fprintf(fi, "MatReal: %u; Direcciona: %d bytes, ", MatReal, Filas*sizeof(
double *));
32 fprintf(fi, "es decir %d punteros a (double *)\n\n", Filas);
33 fprintf(fi, "\n\n");
34 fprintf(fi, "Dirección de las filas:\n");
35 for(i = 0; i < Filas; i++){
36 fprintf(fi, "&MatReal[ %1d]: %u; ", i, &MatReal[i]);
37 fprintf(fi, "MatReal+ %1d: %u; ", i, MatReal+i);
38 fprintf(fi, "MatReal[ %1d]: %u\n", i, MatReal[i]);
39 }
40
41 fclose(fi);
42
43 return 0;
44 }
Filas: 3, Columnas: 3
En la salida se observa que las variables MatReal[0], MatReal[1] y MatReal[2], tienen una serie de
valores almacenados. Estos valores no han sido asignados por el programa sino que que son datos que tenía
previamente almacenados en esa posición de memoria. La figura 6.5 muestra una representación gráfica del
proceso de asignación realizada por el programa 6.14.
2. En el paso segundo, se reserva espacio en la memoria para almacenar los vectores a los que apuntan las
direcciones anteriormente establecidas. Es decir se reserva memoria para las columnas. La instrucción para
direccionar las columnas de la fila i es
J. Sanguino
164 Capítulo – 6. Punteros
3014400 MatReal
2293296 &MatReal
MatReal[0]
XXXXX
3014400
&MatReal[0]
MatReal + 0
MatReal[1]
XXXXX
3014408
&MatReal[1]
MatReal + 1
MatReal[2]
XXXXX
3014416
&MatReal[2]
MatReal + 2
11 fi = fopen("Datos.dat", "w");
12
13 printf("Introduce las Filas y Columnas -> ");
14 scanf(" %d %d", &Filas, &Columnas);
15 while (getchar() != ’\n’);
16
17 fprintf(fi, "Filas: %3d, Columnas: %3d\n\n", Filas, Columnas);
18 fprintf(fi," sizeof(double) = %d; sizeof(double *): %d; sizeof(double **):
%d\n", sizeof(double), sizeof(double*), sizeof(double **));
19 fprintf(fi, "\n\n");
20
21 // *** Direccionamiento de las filas ****
22 MatReal = (double **) malloc( Filas * sizeof(double *));
23 if (MatReal == NULL ) {
24 printf("Error en la asignación de memoria\n");
25 getchar();
26 exit(1);
27 }
28
29 fprintf(fi, "&MatReal: %u; MatReal: %u; \n\n", &MatReal, MatReal);
30 fprintf(fi, "MatReal: %u; Direcciona: %d bytes, ", MatReal, Filas*sizeof(
double *));
31 fprintf(fi, "es decir %d punteros a (double *)\n\n", Filas);
32 fprintf(fi, "\n\n");
33 fprintf(fi, "Dirección de las filas:\n");
34 for(i=0; i<Filas; i++){
35 fprintf(fi, "&MatReal[ %1d]: %u; ", i, &MatReal[i]);
36 fprintf(fi, "MatReal+ %1d: %u; ", i, MatReal+i);
37 fprintf(fi, "MatReal[ %1d]: %u\n", i, MatReal[i]);
38 }
39 fprintf(fi, "\n\n");
40
J. Sanguino
6.7 – Gestión dinámica 165
66 fclose(fi);
67
68 return 0;
69 }
Filas: 3, Columnas: 4
J. Sanguino
166 Capítulo – 6. Punteros
6553344 MatReal
2293280 &MatReal
Es importante destacar que para C, el doble puntero (en este caso MatReal), no se asocia a la idea de matriz
tal y como se planteó en la sección 5.4 (pág. 123). Para el lenguaje C, el único puntero a una matriz es un puntero
a variable de un determinado tipo, cuyo tamaño coincide con el número de columnas de la matriz, tal y como se ha
planteado en la sección 6.4 (pág. 154). Sin embargo, el doble puntero definido a través de la función malloc(),
permite asociar un puntero, a una disposición de datos de doble entrada y que de esta forma, su manejo resulta
muy cómodo.
6.7.2. free()
Cuando se termina de utilizar un bloque de memoria, direccionado previamente por cualquier función de
asignación dinámica, se debe liberar el espacio de memoria para dejarlo disponible para otros procesos. Este
procedimiento lo realiza la función free(). El prototipo que define dicha función es
J. Sanguino
6.8 – Gestión dinámica 167
donde *ptr es el puntero que hace referencia al bloque se hay que liberar. Si ptr es NULL, entonces no hace
nada.
En el programa anterior, para liberar la memoria asignada a la matriz MatReal, se haría también en dos pasos:
1. Se liberaría la memoria asignada a los vectores que direccionan las columnas:
free(MatReal);
6.7.3. calloc()
Se trata de otra función que permite direccionar memoria dinámicamente. Es muy similar a malloc(), de-
vuelve un puntero void* que hace referencia al bloque de memoria direccionado o NULL si no existe memoria
suficiente para asignar. La forma de utilización es
Por ejemplo,
int *PtrInt;
PtrInt = (int *) calloc(25, sizeof(int));
genera una dirección, que almacena en PtrInt, para 25 variables del tipo entero. Tiene unas características
similares a la función malloc().
6.7.4. realloc()
Se trata de una función más técnica que las anteriores. Permite cambiar el tamaño de un bloque de memoria
asignado previamente. Es una función que devuelve un puntero void*. Su forma de utilización es:
J. Sanguino
168 Capítulo – 6. Punteros
Esto sucede porque la pila se ha llenado con las llamadas recursivas y el programa se aborta. Que el tamaño sea
limitado impide que se pueda almacenar en la misma objetos demasiado grandes (como por ejemplo ficheros).
Compartir memoria entre diferentes funciones puede requerir copiar dicha información de forma recurrente,
lo cual es engorroso y consume tiempo.
Para los casos en los que la pila no resuelve nuestro problema tenemos que recurrir a la memoria dinámica.
Para ello el Sistema Operativo (en adelante SO) pone a tu disposición fragmentos de la memoria RAM del equipo.
¿Cómo se solicita uno de esos fragmentos? En el caso de C con malloc o con realloc. Estas funciones hablan con
el SO para solicitar memoria RAM y es el propio SO el que hace la reserva y les indica dónde está ubicada dicha
reserva (el puntero que te devuelven dichas funciones).
Mientras la reserva está en vigor ningún otro programa podrá hacer uso de dicha memoria. Para evitar que un
programa llegue a acaparar de forma innecesaria toda la memoria del equipo es necesario liberar aquella que ya
no es necesaria y para eso se recurre a la función free.
free habla con el SO y le comunica que una de las reservas de memoria que te pertenecen se puede liberar, por
lo que la memoria asociada a dicha reserva puede quedar a disposición de cualquier otro proceso.
Ahora un caso práctico:
int main() while(1) int* ptr = (int*)malloc(1000*sizeof(int));
// operaciones sobre ptr...
Si ejecutas este código ¿qué sucede? Si lo ejecutas y abres el gestor de recursos del sistema verás como la
memoria disponible se consume rápidamente hasta llegar a 0. El ordenador empezará a ir cada vez más lento hasta
que la aplicación finalmente se cierre al no poder reservar más memoria.
Sin embargo si ahora ejecutamos este otro código:
int main() while(1) int* ptr = (int*)malloc(1000*sizeof(int));
// operaciones sobre ptr
free(ptr);
Veremos como el nivel de memoria disponible en el equipo permanece en un nivel estable. El programa ahora
es capaz de realizar reservas de memoria y de liberar dichas reservas, lo que evita que el sistema se quede sin
recursos.
Posibles usos de la memoria dinámica:
creación de árboles y listas. Los sistemas con una cantidad indeterminada de nodos suelen implementarse
con memoria dinámica debido a la flexibilidad que proporciona dicha zona de la memoria (y a que no hay
restricciones de tamaño).
recursos del SO. Muchos de los recursos que el SO pone a tu disposición de ubican en la memoria dinámica.
En estos casos lo normal es que el SO te devuelva un identificador, de tal forma que ni eres consciente del
uso de la memoria dinámica ni tampoco puedes gestionarla directamente... pero ahí está.
Compartición de recursos: Cuando se desean compartir recursos entre diferentes procesos o a nivel global en
la aplicación se suele recurrir a la memoria dinámica para tener más control sobre el ciclo de vida de dichos
recursos.
6.9. Problemas
1. Basándote en el código 6.1 (pág. 144), realiza un programa que declare e inicialice una variable de tipo
double. A continuación declara un puntero a double, que apunte a la variable anterior. A través del puntero,
suma y resta una cantidad constante. En un fichero, escribe el valor inicial de la variable con su dirección y
el resultado final con su dirección.
2. Dado el vector real V = [−2.45, 0.23, −45.98, −23.25, 1.89], se pide que realices un programa que asigne
un puntero al vector y mediante este puntero, opera adecuadamente para incrementar cada componente en
25 unidades. En un fichero de salida se debe escribir el resultado final, con la dirección de memoria de cada
componente. Primero hazlo con un tipo float y después con un tipo double. Observa el valor de las
direcciones de memoria.
J. Sanguino
6.9 – Problemas 169
3. Ejemplo de *++ptr. Basándote en el código 6.7 (pág. 151), se pide que asignes el puntero ptr a la cadena
de caracteres: Me gusta programar en C y a continuación imprime en un fichero, la salida como
consecuencia de operar: *++ptr.
4. Ejemplo de (*ptr)- -. Basándote en el código 6.8 (pág. 152), se pide que asignes el puntero ptr a la
cadena de caracteres: Me gusta programar en C y a continuación imprime en un fichero, la frase
escrita del revés.
6. Escribe un programa que permute las filas dadas de una matriz real dada.
7. En este ejercicio se trata de ilustrar la diferencia del concepto de matriz en C, con el de doble puntero (**
PtrMat).
La conclusión es que el doble puntero se puede utilizar para realizar una distribución matricial de datos,
mediante la función malloc, pero no es una matriz desde el punto de vista riguroso de C. Es interesante
comparar la figura 6.4 (pág. 158) con la figura 6.6 (pág. 6.6). En el primer caso, las direcciones de las filas
y su valor coinciden, mientras que en el segundo no es así.
8. Crea una matriz dinámicamente utilizando la función malloc, de manera que se pueda introducir la matriz
−1.56 5.0 −23.764
A = 34.257 12.34 −25.074
95.72 −84.73 0.34
después escribe en un fichero esta matriz, con las direcciones de memoria de cada una de sus componentes.
J. Sanguino
CAPÍTULO 7
Funciones
7.1. Introducción
La noción de función en un programa está relacionando con la idea de modularidad. Es habitual que los
programas informáticos tengan decenas e incluso cientos de miles de líneas de código fuente. A medida que se van
desarrollando estos programas su tamaño aumenta y se convierten rápidamente en códigos poco manejables. Para
evitar esta situación surge el concepto de modularidad, que consiste en dividir el programa en partes más pequeñas
con una finalidad más concreta. A estos módulos se les suele llamar: subprogramas, subrutinas, procedimientos,
etc. En el lenguaje C se emplea la palabra función. Ya se ha utilizado la idea de función en los programas
presentados hasta ahora (pero no se ha mencionado). En todos ellos, el cuerpo del código, debe escribirse a partir
de la instrucción int main(void) y entre las llaves: { y } . Realmente main consiste en una función, cuya
utilización es obligatoria, por ser la principal. La estructura de las funciones en C es similar.
Un ejemplo muy básico de función en C, aquella que no recibe ni devuelve datos, es la función profesor(),
que se ilustra en el siguiente código:
14 }
15
16 void profesor(void)
17 {
18 printf(" Profesor: estudia y haz los ejercicios ");
19 printf("de programación. No te desanimes\n\n");
20
21 return;
22 }
171
172 Capítulo – 7. Funciones
Quiero aprender C.
La función definida se llama profesor y tal como expresa el prototipo de la función (LÍNEA 3) es una función
que no contiene argumentos (igual que la función main), ni devuelve ningún valor debido a la palabra void,
antes del nombre de profesor. La definición de la función está en el fichero fuente, a partir de la LÍNEA 16. La
llamada a la función se hace por su nombre, en la LÍNEA 9, en el código principal. En la LÍNEA 3, se presenta lo que
se llama prototipo de la función. Indica al compilador que se va a definir una función, en este caso, sin argumentos
y sin ningún valor de retorno.
Es claro que este tipo de funciones tienen una utilidad más que discutible. Las funciones más interesantes serán
aquellas que permitan transmitir valores de variables, para que sean manipuladas y después devuelvan sus nuevos
valores al programa principal.
En este capítulo se analizarán los siguientes conceptos relacionados con el concepto de función:
Alcance local de las variables. En C, las variables definidas en el programa principal y las funciones son locales.
Esto quiere decir, que sólo son visibles en aquellas partes en dónde se definen, aunque tengan el mismo
nombre.
Intercambio de datos. Una función se activa mediante una llamada del programa principal o de otra función que
esté a un nivel superior. En C se hace a través del nombre de la función. Para intercambiar información entre
variables de distintas funciones, se puede hacer a través de los argumentos de la función o mediante el tipo
de valor que puede retornar una función.
sentencias ejecutables
...
...
sentencias ejecutables
return (expresión);
}
La primera línea se conoce como el encabezamiento (header) de la función y todo aquello que está entre llaves,
recibe el nombre de cuerpo (body). Se muestra con más detalle, algunos de los elementos que forman la estructura
de una función.
J. Sanguino
7.2 – Aspectos fundamentales 173
tipo_de_retorno
Resulta que en C, cuando una función es ejecutada puede devolver al programa o función que la ha llamado, un
valor (valor de retorno) cuyo tipo, debe ser especificado en el encabezamiento de la función y se llama tipo de
retorno de la función. Si no se establece el tipo de retorno de la función, entonces se considera que la función
devolverá un valor entero. Es decir, el tipo de retorno es int, mientras no se especifique lo contrario. Si se quiere
establecer explícitamente que la función no devuelva ningún valor, entonces el tipo de retorno que debe declararse
es void.
Sólo es necesario incluir los tipos de las variables que formarán el argumento. No se necesitan la identificación
de las variables. Los prototipos de las funciones permiten al compilador que realice correctamente la conversión
del tipo del valor del retorno. Este modelo de declaración suele hacerse al comienzo del fichero, después de las
directivas #define y #include.
J. Sanguino
174 Capítulo – 7. Funciones
está en construir una función, cuyo argumento de entrada sea el valor n. Una posible solución se muestra en el
código 7.2
En este ejemplo, resulta importante destacar la LÍNEA 3 que es en dónde se define el prototipo de la función.
Posteriormente en la LÍNEA 19 se hace la llamada a la función SumaNaturales(N) (con el argumento real N),
que lleva el control del programa a la LÍNEA 24 en donde empieza la definición de la función.
Una posible mejora del programa es que la salida se produzca en un fichero de datos. Para ello se introduce
un nuevo argumento en la función SumaNaturales que sea el puntero a fichero. En el siguiente código 7.3 se
muestra el procedimiento:
J. Sanguino
7.2 – Aspectos fundamentales 175
4
5 int main(void)
6 {
7 int N;
8 int chk;
9
10 FILE *fi;
11
12 fi = fopen("Datos.dat", "w");
13
14
15 // **** Entrada de datos ************
16 do {
17 printf("\n\n Introduce un número natural (N > 1) -> ");
18 chk = scanf(" %i", &N);
19 while(getchar() != ’\n’);
20 } while(chk != 1 || N < 2 );
21 printf("\n\n");
22
23 // *** LLamada a la función ***
24 SumaNaturales(fi, N); //** Argumentos REALES
25
26 fclose(fi);
27
28 return 0;
29 }
30 void SumaNaturales(FILE *fo, int K) //** Argumentos FORMALES
31 {
32 int Suma;
33 int i;
34
35 for (i = 0, Suma = 0; i <= K; i++){
36 Suma += i;
37 }
38 fprintf(fo, "La suma de los %i primeros números naturales es %i\n\n",
39 K, Suma);
40
41 return;
42 }
en la pantalla aparecería
Otra alternativa sería que el valor de la suma retornara al programa principal y ahí, imprimirlo. Para ello hay
que dotar a la función SumaNaturales de un tipo de retorno. Es lo que se ha llamado tipo de la función. En el
código 7.4 se observa este recurso:
J. Sanguino
176 Capítulo – 7. Funciones
6 {
7 int N, Suma = 0;
8 int chk;
9
10 FILE *fi;
11
12 fi = fopen("Datos.dat", "w");
13
14
15 // **** Entrada de datos ************
16 do {
17 printf("\n\n Introduce un número natural (N > 1) -> ");
18 chk = scanf(" %i", &N);
19 while(getchar() != ’\n’);
20 } while(chk != 1 || N < 2 );
21 printf("\n\n");
22
23 // *** LLamada a la función
24 Suma = SumaNaturales(N); //*** Argumento REAL
25
Merece la pena, detenerse un momento en la definición de la función SumaNaturales, situada en las LÍNEAS
33-44. En el encabezamiento de la función (LÍNEA 33) ya aparece el tipo de la función, que corresponde a un int.
Además se define la variable Resultado (LÍNEA 35) con el objetivo de almacenar el resultado final de la suma.
Por último, su valor se devuelve al programa principal, a través de la instrucción return y se asigna a la variable
Suma (LÍNEA 24) en el programa principal.
Es importante hacer notar que lenguaje C asigna distintas posiciones de memoria a las variables definidas en
las subrutinas o funciones, respecto de las definidas en el programa principal. Esto es, aunque en la definición de la
función SumaNaturales se hubiese utilizado el nombre de Suma en lugar de Resultado, para C la posición
de memoria sería distinta a la ocupada por la variable Suma del programa principal y por tanto, distintas. Este
tema se abordará con más detalle en la sección 7.3 (pág. 185).
J. Sanguino
7.2 – Aspectos fundamentales 177
Según la opción elegida por el usuario, el programa puede seguir una ejecución u otra distinta. Un algoritmo
general que permite abordar esta idea, resulta sencillo
obtén letra;
haz mientras letra distinta ’q’
switch según letra y ejecuta función;
siguiente selección de letra;
fin haz mientras;
Un ejemplo donde poner en práctica este algoritmo puede seguirse en el libro de Prata [16, pág. 325]. El
objetivo es utilizar este procedimiento junto con una función que permita leer correctamente los datos de entrada.
De esta manera, la construcción del menú se descompone en funciones sencillas resultando más asequible la
codificación.
En el siguiente código se define una función llamada selecciona() que establece el diseño del menú y
obtiene la opción seleccionada por el usuario. En este caso, la función devuelve un tipo char, pero no tiene
argumentos de entrada.
J. Sanguino
178 Capítulo – 7. Funciones
30
31 } //Fin while de entrada de selección
32
33 return 0;
34 }
35
36 char selecciona(void){
37
38 char ch;
39
40 printf("Por favor selecciona una letra:\n\n\n ");
41 printf("a. Tarta de manzana; b. Helado de chocolate;\n\n ");
42 printf("c. Cafe q. Salir\n");
43 ch = getchar();
44 while(getchar() != ’\n’);
45 while( (ch < ’a’ || ch > ’z’) && ch != ’q’){
46 printf("ERROR...Por favor: a; b; c, o q (para salir) -> ");
47 ch = getchar();
48 while(getchar() != ’\n’);
49 }
50 return (ch);
51
52 }
El modelo de código es similar a los ejemplos anteriores. En la LÍNEA 3 se muestra el prototipo de la función
y a partir de la LÍNEA 36 se establece la definición explícita. En este caso, únicamente retorna un carácter que
corresponde a la opción elegida por el usuario.
Dentro de las definiciones de las funciones es posible llamar a otras funciones. Por ejemplo, siguiendo con el
ejemplo anterior. Puede definirse una nueva función que lea un carácter e inmediatamente borre el buffer. Esto es,
que junte las instrucciones
ch = getchar();
while(getchar() != ’\n’);
12 switch (letra){
13
14 case ’a’:
15 printf("Has seleccionado la tarta de manzana. ");
16 printf("Buena eleccion\n\n");
17 break;
18 case ’b’:
19 printf("Has seleccionado el helado de chocolate. ");
20 printf("Te gusta el chocolate\n\n");
21 break;
J. Sanguino
7.2 – Aspectos fundamentales 179
22 case ’c’:
23 printf("Has seleccionado el cafe. ");
24 printf("La opcion más barata\n\n");
25 break;
26 default :
27 printf("Oouups!!!. ");
28 printf("No contaba con esto.....\n\n");
29 break;
30 }
31 } //Fin while de entrada de selección
32
33 return 0;
34 }
35
36 char selecciona(void){
37
38 char ch;
39
40 printf("Por favor selecciona una letra:\n\n\n ");
41 printf("a. Tarta de manzana; b. Helado de chocolate;\n\n ");
42 printf("c. Cafe q. Salir\n");
43 ch = lee_letra();
44 while( (ch < ’a’ || ch > ’z’) && ch != ’q’){
45 printf("ERROR...Por favor: a; b; c, o q (para salir) -> ");
46 ch = lee_letra();
47 }
48
49 return (ch);
50
51 }
52
53 char lee_letra(void){
54
55 char ch;
56
57 ch = getchar();
58 while(getchar() != ’\n’);
59
60 return (ch);
61 }
De nuevo, esta última función únicamente devuelve un carácter y no tiene ningún tipo de argumentos. Como
se observa es llamada dentro de la función selecciona(), previamente definida.
J. Sanguino
180 Capítulo – 7. Funciones
7 int main(void)
8 {
9
10 int a = 10, b = 0, c = -22;
11 int d, e, f;
12
13 FILE *fi;
14 fi = fopen("Datos.dat", "w");
15
16 d = absoluto(a);
17 e = absoluto(b);
18 f = absoluto(c);
19 fprintf(fi, "Los valores absolutos de %d; %d; %d ", a, b, c);
20 fprintf(fi, " son: %d; %d; %d\n", d, e, f);
21
22 fclose(fi);
23
24 return 0;
25 }
26
La definición de la función absoluto está entre las LÍNEAS 28-33. En el encabezamiento se ha realizado una
declaración específica sobre el tipo de retorno, en este caso int. Se insiste de nuevo, que el argumento: int
x, que aparece en la definición, corresponde al argumento formal. Sin embargo, en las llamadas a la función
absoluto (LÍNEAS 16–18), las variables a, b, c son argumentos reales, respectivamente. En la LÍNEA 5 se
encuentra definido el prototipo de la función absoluto.
Tal y como se ha planteado el código anterior, la función absoluto sólo está definida para valores enteros.
Es lógico, ampliar el espectro de esta función, considerando también números reales que puedan introducirse por
teclado. Además la definición se puede compactar, eliminado la declaración de la variable y. Otra versión un poco
más realista sería:
J. Sanguino
7.2 – Aspectos fundamentales 181
23 return 0;
24 }
25
26 /* Función valor absoluto */
27 double absoluto(double x)
28 {
29 return ( (x < 0) ? -x : x );
30 }
Ejemplo: potencia de 2
Otro ejemplo, consiste en definir una función que calcule la potencia entera de 2. La función se define con tipo
long long y tiene como argumentos la base, que se toma como constante el valor de 2 y un valor entero, que es
el exponente. El resultado se imprime en un fichero de datos. La codificación puede verse en el programa 7.9.
14 FILE *fi;
15
16 fi = fopen("Datos.dat", "w");
17
18 // **** Entrada de datos ************
19 do {
20 printf("\n\n Introduce un número natural (N > 1) -> ");
21 chk = scanf(" %i", &N);
22 while(getchar() != ’\n’);
23 } while(chk != 1 || N < 2 );
24 printf("\n\n");
25
J. Sanguino
182 Capítulo – 7. Funciones
30
31 }
32 long long f_potencia(unsigned k, int b)
33 {
34 int incremento;
35 long long potencia;
36
37 for(incremento = 1, potencia = 1.; incremento <= b; incremento++){
38 potencia = potencia * k;
39 }
40 return (potencia);
41 }
La potencia 25 de 2 es 33554432
Ejemplo: factorial
El cálculo del factorial de un número, ya tratado en ejercicios anteriores (véase por ejemplo, sección 4.7 de
Problemas, pág. 92), se puede calcular a través de una función, muy fácilmente. En este ejemplo, se añade una
función que escriba en un fichero de salida, los datos del alumno junto con el valor del factorial. Un posible
programa se muestra en el código 7.10.
J. Sanguino
7.2 – Aspectos fundamentales 183
29
30 // *** Imprime el resultado a través de la función Factorial
31 fprintf(fi, "El factorial de %i es %i! = %i", N, N, Factorial(N));
32
33 fclose(fi);
34
35 return 0;
36
37 }
38
39 void Presentacion(FILE *fo)
40 {
41 fprintf(fo, "\n **** Apellidos: AAAAAAA \n");
42 fprintf(fo, "\n **** Nombre: AAAAA\n");
43 fprintf(fo, "\n **** Numero Mat: 111111\n");
44 fprintf(fo, "\n ***************************\n\n");
45
46 return;
47 }
48
***************************
J. Sanguino
184 Capítulo – 7. Funciones
f (t) t2 + x t x 1 x
g(t) = t − = = + = t +
f 0 (t) 2t 2 2t 2 t
tn+1 = g(tn )
que genera una sucesión convergente a la solución2 (véase por ejemplo, Mathews-Fink [15, 80]). Para establecer
una aproximación a la solución, se considera como criterio de parada:
|t2n − x| < ε
J. Sanguino
7.3 – Visibilidad de las variables 185
37
38 double absoluto(double x) // *** Argumento FORMAL
39 {
40 return ( (x < 0) ? -x : x ); /* Devuelve el valor de y a main()*/
41 }
42
43 double Raiz2(double x) // *** Argumento FORMAL
44 {
45 double t_i = 1.;
46 const double epsilon = .000001;
47
48
49 // *** Se utiliza la función absoluto()
50 while ( epsilon <= absoluto(t_i * t_i - x) ){
51 t_i = (t_i + x/t_i) / 2.;
52 }
53 return (t_i);
54 }
Si se ejecuta el programa sin introducir los datos por pantalla, el resultado en el fichero Datos.dat es
En la LÍNEA 50 la función Raiz2 hace uso de la función absoluto que también se ha definido previamente.
Para el compilador, no es importante el orden en la que se hayan definido. Lo importante es que estén localizables.
En este caso en el mismo fichero fuente que el programa principal.
J. Sanguino
186 Capítulo – 7. Funciones
17
18 // **** Entrada de datos ************
19 do {
20 printf("\n\n Introduce un número natural (N > 1) -> ");
21 chk = scanf(" %i", &Dato);
22 while(getchar() != ’\n’);
23 } while(chk != 1 || Dato < 2 );
24 printf("\n\n");
25
26 fprintf(fi, "Antes de entrar en la función\n");
27 fprintf(fi, "Dato = %i, Dirección: %u", Dato, &Dato);
28 fprintf(fi, "\n\n");
29
30 Prueba(fi, Dato); //*** Argumento REAL
31
32 fprintf(fi,"\n\n");
33 fprintf(fi, "Estoy de nuevo, en el programa principal\n");
34 fprintf(fi, "Dato = %i, Dirección: %u", Dato, &Dato);
35 fprintf(fi, "\n\n");
36
37 fclose(fi);
38 return 0;
39
40 }
41 void Prueba(FILE *fo, int Dato) //*** Argumento FORMAL
42 {
43 fprintf(fo, "Estoy dentro de la función Prueba\n");
44 fprintf(fo, "Dato = %i, Dirección: %u\n", Dato, &Dato);
45 Dato = Dato + 20;
46 fprintf(fo, "Dato + 20 = %i, Dirección: %u", Dato, &Dato);
47 return;
48 }
En este código se declara la variable Dato en el programa principal (LÍNEA 11). Posteriormente, por teclado se
le asigna un valor de 15. Con este valor entra en la función Prueba (LÍNEA 30), en la que también existe una
variable con el mismo nombre de Dato (LÍNEA 41), que recibe el valor de 15. En esta función se modifica el valor
de la variable Dato a 35 (LÍNEA 45) y se devuelve el control al programa principal. Posteriormente se imprime
el valor de la variable Dato y tiene el valor de 15 (LÍNEA 34). Esto es así, porque como se observa en la salida
del programa, impresa en fichero Salida.dat, la variable Dato, definida en el programa principal y la variable
Dato, declarada en la función Prueba tienen direcciones de memoria distintas. Esto implica que para C, aunque
tienen el mismo nombre son variables distintas3 .
Una interpretación gráfica de este proceso puede verse en la figura 7.1
3
Los humanos también somos capaces de distinguir personas conocidas, con el mismo nombre.
J. Sanguino
7.3 – Visibilidad de las variables 187
main
Dato
15
Prueba(fi, Dato)
2293312
&Dato
Prueba
Dato Dato + 20 Dato
15 35
return 2293288
&Dato
Si las variables se declaran como static (estáticas), entonces aunque tienen la misma visibilidad que las auto,
se diferencian en que su valor permanece en memoria. Esto es, su valor no desaparece cuando la función que las
contiene, finaliza su trabajo; el programa recuerda su valor, si la función vuelve a ser llamada. Las variables que
son visibles en todo momento y en toda parte del programa se llaman globales y en C se declaran como extern
(externas). El programa 7.13 muestra el efecto de los tres tipos de visibilidad en las variables.
13 int i;
14 FILE *fi;
15
16 fi = fopen("Datos.dat", "w");
17
18 for (Var_extern = 1, i = 0; i < 7; i++) {
19 Var_extern++;
20 Auto_static_extern(fi);
21 }
22
23
24 fclose(fi);
25 return 0;
26
27 }
28
29 void Auto_static_extern(FILE *fo)
30 {
31 int Var_auto = 1;
32 static int Var_static = 1;
33 extern int Var_extern;
34
J. Sanguino
188 Capítulo – 7. Funciones
En este ejemplo se observa claramente el efecto de los tres tipos de variables. Es interesante destacar la definición
de la variable Var_extern. Su declaración se realiza en la LÍNEA 8, antes de la función main. Posteriormente
se inicializa en la LÍNEA 18 y dentro del bloque for se aumenta en una unidad. El efecto de su globalidad es
inmediato al entrar en la función Auto_static_extern, pues mantiene su valor y además aumenta en una
unidad antes de regresar al programa principal. Seguidamente vuelve a aumentar en una unidad y antes de entrar
de nuevo en la función Auto_static_extern y vuelve a mantener este valor dentro de la función. Por esto en
la salida, su incremento es de dos unidades en dos unidades. También se observa claramente el efecto de considerar
una variable static. Una vez declarada e inicializada en la función mantiene su valor cada que se vuelve a entrar en
la función.
En principio, las variables tipo static y extern no serán utilizadas en este documento.
Pila (Stack)
J. Sanguino
7.4 – Regiones de la memoria 189
Esta distinción es un tanto arbitraria. Otros autores la descomponen en dos grupos únicamente: área de tamaño
fijo y área tamaño variable. En esta última, agrupan la pila y la gestión dinámica. En este documento, se seguirá
la planteada inicialmente, que suele ser la más habitual (véase la figura 7.2).
Direcciones altas
Stack
Heap
Código
Direcciones bajas
J. Sanguino
190 Capítulo – 7. Funciones
• El tamaño de los objetos debe ser conocido en tiempo de compilación. Esto implica que no se
puede trabajar con objetos de longitud variable. Por lo general, es difícil para el programador defi-
nir el tamaño de las estructuras que va a usar. En efecto, si son demasiado grandes, se desperdicia
memoria; si son pequeñas, no puede utilizar el programa en todos los casos.
• No se pueden implementar procedimientos recursivos.
Los inconvenientes planteados para las variables situadas en el segmento estático, hace que sea necesario
implementar una gestión dinámica en la memoria durante el proceso de ejecución. Esto es lo que hace el C,
en dos regiones llamadas Pila y Montón o Área de gestión dinámica, que se estudian a continuación.
Pila (Stack). Como ya se ha visto, en C una variable definida en una función (sin especificador o bien con el de
auto), no es visible en la función previa que ha realizado la llamada a la primera. Es decir, que la variable
desaparece cuando la ejecución del programa sale de la función en donde ha sido definida.
Las variables con estas características se conocen como automáticas y en C se construyen con el especifica-
dor auto o bien, sin añadir ninguno. Debido a la naturaleza de estas variables, su gestión debe ser dinámica
pues, hay momentos en los que el programa puede acceder a ellas y en otros no.
Por esta razón, existe una región de memoria específica, llamada Pila o Stack, para almacenar estas varia-
bles. Los argumentos y las variables locales, son asignados y desasignados de manera dinámica durante la
ejecución de las correspondientes funciones. Esto se realiza de manera automática por el código generado
por el compilador, sin que el programador tenga acceso a la gestión.
La mecánica de esta gestión de los datos se ajusta a un proceso de naturaleza LIFO (Last Input– First Out-
put), que resulta muy eficiente y rápido. Permite direccionar eficazmente variables que serán usadas fre-
cuentemente y a la vez posibilita ahorrar espacio de direccionamiento, ya que puede reutilizar el espacio de
memoria dedicado a la función, cuando ésta termina. Además posibilita el diseño de funciones recursivas y
reentrantes, asociando un espacio diferente para las variables por cada llamada de la función.
Segmento de gestión dinámica (Heap). Se trata de un área de memoria al que tiene acceso el programador me-
diante las función malloc y calloc, a través de punteros. Se accede a esta memoria en tiempo de eje-
cución, lo cual hace que los programas sean muy flexibles, pero tiene el inconveniente que el acceso puede
ralentizarse, ya que durante la ejecución deben buscarse las direcciones libres. Es por ello, que resulta fun-
damental liberar los espacios que dejan de ser utilizados durante la ejecución, mediante la función free.
Desde un punto de vista de la programación, con este tipo de variables, resulta crucial la utilización ade-
cuada de free. En efecto, hay que destacar (véase el libro de Prata [16, pág. 547]) que la cantidad de
memoria asignada al área de tamaño fijo durante el tiempo de ejecución es fija (como su propio nombre
indica), además la cantidad de memoria utilizada por las variables automáticas puede crecer y disminuir
automáticamente durante la ejecución del programa. Sin embargo, la cantidad de memoria utilizada por el
segmento de gestión dinámica, únicamente crece a menos que se utilice la función free. Si no se realiza
este proceso se puede llegar a una situación conocida como falta de memoria del sistema (memory leaks).
J. Sanguino
7.5 – Paso de argumentos 191
8
9 int main(void)
10 {
11
12 int a, b;
13 FILE *fi;
14
15 fi = fopen("Datos.dat", "w");
16
17 a = 12;
18 b = -35;
19 fprintf(fi, "En el programa principal se han asignado los valores\n");
20 fprintf(fi, "a = %3i &a = %8u\n", a, &a);
21 fprintf(fi, "b = %3i &b = %8u", b, &b);
22
23 fprintf(fi, "\n\n");
24 Intercambio(fi, a,b);
25 fprintf(fi, "\n\n");
26
27 fprintf(fi, "En el programa principal los nuevos valores son:\n");
28 fprintf(fi, "a = %3i &a = %8u\n", a, &a);
29 fprintf(fi, "b = %3i &b = %8u\n", b, &b);
30
31
32 fclose(fi);
33 return 0;
34
35 }
36
37 void Intercambio(FILE *fo, int x, int y)
38 {
39 int temp;
40
41 fprintf(fo, " Valores recibidos del programa principal:\n");
42 fprintf(fo, " a = %3i &a = %8u\n", x, &x);
43 fprintf(fo, " b = %3i &b = %8u\n", y, &y);
44
45 temp = x;
46 x = y;
47 y = temp;
J. Sanguino
192 Capítulo – 7. Funciones
48
49 fprintf(fo, " Una vez realizado el intercambio sus valores son:\n");
50 fprintf(fo, " a = %3i &a = %8u\n", x, &x);
51 fprintf(fo, " b = %3i &b = %8u\n", y, &y);
52 fprintf(fo, " Se devuelve el control al programa principal");
53
54 return;
55 }
Tal y como está desarrollado el código, no se obtiene el resultado propuesto, porque como se observa en las
dos últimas líneas del fichero de salida, los valores de las variables a y b, no han sido intercambiados. Sin em-
bargo, sí puede apreciarse en el fichero de salida, que los valores fueron intercambiados dentro de la función
Intercambio.
Esto es consecuencia de la naturaleza automática de las variables, como ya se comentó en la sección 7.3, llama-
da Visibilidad de las variables (pág. 185). Las variables declaradas en las funciones tienen posiciones de memoria
distintas, a las declaradas en otro programa o en el programa principal y por tanto son variables diferentes. Esta
circunstancia puede apreciarse de nuevo, en el fichero Datos.dat.
Para solucionar esta contrariedad, se plantea no pasar los valores de las variables, sino las direcciones de las
variables, como se muestra ahora en programa 7.15.
12 int a, b;
13 FILE *fi;
14
15 fi = fopen("Datos.dat", "w");
16
17 a = 12;
18 b = -35;
19 fprintf(fi, "En el programa principal se han asignado los valores\n");
20 fprintf(fi, "a = %3i &a = %8u\n", a, &a);
21 fprintf(fi, "b = %3i &b = %8u", b, &b);
J. Sanguino
7.5 – Paso de argumentos 193
22
23 fprintf(fi, "\n\n");
24 Intercambio(fi, &a, &b);
25 fprintf(fi, "\n\n");
26
27 fprintf(fi, "En el programa principal los nuevos valores son:\n");
28 fprintf(fi, "a = %3i &a = %8u\n", a, &a);
29 fprintf(fi, "b = %3i &b = %8u\n", b, &b);
30
31
32 fclose(fi);
33 return 0;
34
35 }
36
37 void Intercambio(FILE *fo, int *p1, int *p2)
38 {
39 int temp;
40
41 fprintf(fo, " Valores recibidos del programa principal:\n");
42 fprintf(fo, " a = %3i &a = %8u\n", *p1, p1);
43 fprintf(fo, " b = %3i &b = %8u\n", *p2, p2);
44
45 temp = *p1;
46 *p1 = *p2;
47 *p2 = temp;
48
49 fprintf(fo, " Una vez realizado el intercambio sus valores son:\n");
50 fprintf(fo, " a = %3i &a = %8u\n", *p1, p1);
51 fprintf(fo, " b = %3i &b = %8u\n", *p2, p2);
52 fprintf(fo, " Se devuelve el control al programa principal");
53
54 return;
55 }
J. Sanguino
194 Capítulo – 7. Funciones
para transmitir automáticamente las direcciones de memoria de estas variables. Se llama paso de argumentos por
referencia. Como ya se ha comentado, el C no lo permite y hay que especificar explícitamente que se transmite
una dirección (por medio de punteros). Por eso se dice, que en C, el paso de argumentos a las funciones se hace
siempre por valor.
7.6.1. Ejemplos
Para ilustrar esta idea, se plantean unos ejemplos en los que se opera con funciones en cuyos argumentos
aparecen direcciones de vectores.
17 FILE *fi;
18
19 fi = fopen("Datos.dat", "w");
20
21 double Vector[DIM];
22
J. Sanguino
7.6 – Funciones y vectores 195
33
34 Minimo = VecMin(Vector, DIM); //*** Argumentos REALES
35
36 fprintf(fi,"El valor más pequeño es %.5f\n", Minimo);
37
38
39 fclose(fi);
40 return 0;
41
42 }
43
44 double VecMin(double *P, int N) // *** Argumentos FORMALES
45 {
46 double Min;
47 int i;
48
49 for (Min = P[0], i = 1; i < N; i++){
50 if (P[i] < Min){
51 Min = P[i];
52 }
53 }
54
55 return (Min);
56 }
Como se observa, una vez dimensionado e inicializado el vector (LÍNEAS 21-25), en la llamada a la función (LÍNEA
34) se introduce el nombre (Vector), que junto con el tamaño del vector (DIM), permite acceder a cualquier
componente, dentro de la función. Es importante destacar, que en la definición de la función VecMin (LÍNEAS
44–56), se utiliza no un vector, sino un puntero a double (P) cuyo cometido es almacenar la dirección de la
primera componente (véase (6.2), pág. 149). Gracias a este hecho, se establecen las equivalencias o igualdades (6.1)
(pág. 149), lo que permite, en el programa VecMin, expresar el puntero P como si se tratase de un vector.
8
9 int main(void)
10 {
11
J. Sanguino
196 Capítulo – 7. Funciones
20 double Vector[DIM];
21
22 Vector[0] = -23.45, Vector[1] = 467,
23 Vector[2] = -123, Vector[3] = 23,
24 Vector[4] = 0;
25
A diferencia del ejemplo anterior, ahora la función VecOrden no devuelve ningún valor. Sin embargo, el proce-
J. Sanguino
7.7 – Funciones y matrices 197
dimiento para operar con el vector es el mismo, que el del programa previo 7.16. Como argumento formal se toma
un puntero a double que se opera como si fuese un vector. Al operar sobre las direcciones de memoria, el resultado
se verá reflejado en el argumento real, en este caso, la variable vectorial Vector.
10 //*** Prototipo
11 void ProdMat(int, int, int, int (* )[], int (* )[], int (* )[]);
12
13 int main(void)
14 {
15 int i, j, k;
16
17
18 int A[N][P] = {{1, 2}, {3, 4}, {5, 6}};
19 int B[P][M] = {{-4, 3, -7}, {-1, 11, 2}};
20 int C[N][M] = {0};
21
22 FILE *fi;
23
24 fi = fopen("Datos.dat", "w");
25
26 fprintf(fi,"Matriz A: \n");
27 for (i = 0; i < M; i++){
28 for(k = 0; k < P; k++){
29 fprintf(fi, " %4i", A[i][k]);
30 }
31 fprintf(fi, "\n");
J. Sanguino
198 Capítulo – 7. Funciones
32 }
33
52 //Salida de resultados
53 fprintf(fi,"\n ********* RESULTADO ******* \n\n");
54 fprintf(fi, "Matriz Producto C:\n");
55 for(i = 0; i < N; i++){
56 for(j = 0; j < M; j++){
57 fprintf(fi, " %4i", C[i][j]);
58 }
59 fprintf(fi, "\n");
60 }
61
62 fclose(fi);
63
64 return 0;
65 }
66 //*** Definición de la función con argumentos FORMALES
67 void ProdMat(int F, int C, int S,
68 int (*MatA)[S], int (*MatB)[C], int (*MatC)[C])
69 {
70 int i, j, k;
71
72 for (i = 0; i < F; i++){
73 for(j = 0; j < C; j++){
74 for(MatC[i][j] = 0, k = 0; k < S; k++){
75 MatC[i][j] += MatA[i][k] * MatB[k][j];
76 }
77 }
78 }
79 return;
80 }
Matriz A:
1 2
3 4
5 6
Matriz B:
-4 3 -7
-1 11 2
Matriz producto C, inicial:
0 0 0
J. Sanguino
7.8 – Punteros a funciones 199
0 0 0
0 0 0
Matriz Producto C:
-6 25 -3
-16 53 -13
-26 81 -23
En la definición de la función ProdMat (LÍNEAS 67–80), se utilizan punteros matriciales que pueden expresarse
en forma matricial (véase (6.3), pág. 157), lo que simplifica la programación del algoritmo. Por otra parte, en la
llamada a la función (LÍNEA 50), la dirección de la matriz producto C, entra como un argumento más. Esto permite
que cuando se realice el producto matricial en la función, los valores se sitúen en las direcciones de la matriz C,
obteniéndose el resultado deseado, cuando se devuelve el control de la ejecución al programa principal.
indica que se trata de una función que devuelve un puntero al tipo declarado. Por tanto, no es lo mismo. La ventaja
de utilizar punteros a funciones es que permite utilizar las funciones como parámetros de otras funciones. Ejemplos
de esta situación se encuentran frecuentemente en Cálculo numérico. Un método de integración o un método de
resolución de ecuaciones depende de la función o de la ecuación a resolver. Los punteros a función permiten
escribir una función genérica, que reciba como parámetro un puntero a la función o ecuación con la que ha de
operarse.
Veamos primeramente el ejemplo del Método de la secante, que es un procedimiento básico en Cálculo numé-
rico, para la resolución de una ecuación no lineal (véase Mathews y Fink [15]). En este caso es necesario introducir
la ecuación. En el código 7.19 la función asociada, entra como parámetro:
J. Sanguino
200 Capítulo – 7. Funciones
14 FILE *fi;
15 fi = fopen("Datos.dat", "w");
16
17 printf("Introduce p0 -> ");
18 scanf(" %lf", &p0);
19 while(getchar() != ’\n’); /* Vacía el buffer */
20 printf("\n\n");
21 printf("Introduce p1 -> ");
22 scanf(" %lf", &p1);
23 while(getchar() != ’\n’); /* Vacía el buffer */
24
25 fprintf(fi, " p1: %f y p0: %f\n\n", p1, p0);
26 solfin = f_secante(fi, p0, p1, fun);
27
28 fprintf(fi, "\n\n");
29 fprintf(fi, "La solución es: %f\n", solfin);
30
31 fclose(fi);
32
33 return 0;
34
35 }
36 /* Función asociada a la ecuación */
37 double fun(double x)
38 {
39 double y;
40
41 y = pow(x,3) - 2.*pow(x,2)- x + 2;
42 return (y);
43 }
44 /* Función que codifica el método de la secante*/
45 double f_secante(FILE *fo, double x0, double x1, double (* f)(double ))
46 {
47 int iter = 0;
48 int MaxIter = 500;
49 double delta = 1.e-9, epsilon = 1.e-9;
50 double dif = 1., val = 1.;
51 double sol = 0.;
52
53 while(delta < dif && epsilon < val && iter++ <= MaxIter){
54 sol = x1 - f(x1)*(x1-x0)/( f(x1)-f(x0) );
55 dif = fabs(sol-x1);
56 val = fabs(f(sol));
57 x0 = x1;
58 x1 = sol;
59 fprintf(fo,"dif: %.20f; val: %.20f; ", dif, val);
60 fprintf(fo, "sol: %.10f; Iter: %d\n", sol, iter);
61 }
62 return (sol);
63 }
J. Sanguino
7.8 – Punteros a funciones 201
f (x) = x3 − 2x2 − x + 2,
que queda definida en el programa en las LÍNEAS 37-43, mediante el nombre de fun. Para establecer el código
del método de la secante se utiliza la función f_secante en las LÍNEAS 45-63. En el último argumento de la
definición de esta función (LÍNEA 45), se establece un puntero a función de tipo double, mediante la expresión
double (* f)(double )
Posteriormente, en la llamada (véase LÍNEA 26), el nombre de la función: fun actúa como puntero.
En algunos casos es necesario introducir más de una función, como sucede en el método de Newton–Raphson,
también un algoritmo básico, para la resolución de ecuaciones no lineales. En este procedimiento es necesario
disponer de la función que define la ecuación y su derivada. Es posible resolver esta situación de manera análoga
al caso anterior. Sin embargo, puede resultar más útil, en ciertas ocasiones definir un vector de punteros a funciones.
Seguiremos este procedimiento. Para ello se establece el programa 7.20:
24 PtrF[0] = fun;
25 PtrF[1] = dfun;
26 solfin = f_NR(fi, p0, PtrF);
27
28 fprintf(fi, "\n\n");
29 fprintf(fi, "La solución es: %f; \n", solfin);
30
31 fclose(fi);
32
33 return 0;
J. Sanguino
202 Capítulo – 7. Funciones
34
35 }
36
37 double fun(double x)
38 {
39 double y;
40
41 y = pow(x,3) - 2.*pow(x,2)- x + 2;
42 return (y);
43 }
44
45 double dfun(double x)
46 {
47 double y;
48
49 y = 3*pow(x,2) - 4*x -1;
50 return (y);
51 }
52
53 double f_NR(FILE *fo, double x0, double (* f[2])(double ))
54 {
55 int iter = 0;
56 int MaxIter = 500;
57 double delta = 1.e-9, epsilon = 1.e-9;
58 double dif = 1., val = 1.;
59 double sol = 0.;
60
61 while(delta < dif && epsilon < val && iter++ <= MaxIter){
62 sol = x0 - f[0](x0)/f[1](x0);
63 dif = fabs(sol-x0);
64 val = fabs(f[0](sol));
65 x0 = sol;
66 fprintf(fo,"dif: %.15f; val: %.15f; ", dif, val);
67 fprintf(fo, "sol: %.10f; Iter: %d\n", sol, iter);
68 }
69 return (sol);
70 }
Siguiendo con la ecuación anterior (7.1), la función asociada a la ecuación y su derivada, se definen a través de
las funciones: fun y dfun, respectivamente. El objetivo ha sido definir un vector de punteros a función, con dos
componentes: una que apunte a fun y otra que señale a dfun. La declaración de este vector, se plantea en la
LÍNEA 12 mediante la sentencia
double (*PtrF[2])();
Posteriormente, a este vector se le asigna las direcciones (mediante el nombre) de las funciones fun y dfun
(LÍNEAS 24-25). El vector de punteros a funciones: PtrF entra como tercer argumento en la llamada a la función
J. Sanguino
7.9 – Recursividad 203
f_NR (véase LÍNEA 26), que resuelve el algoritmo de Newton-Rapshon. En la declaración de variables para el sub-
programa f_NR (LÍNEA 53) se define el vector de dos componentes de punteros a función, mediante la expresión:
double (* f[2])(double )
7.9. Recursividad
Un método se dice recursivo cuando está basado en un procedimiento que al aplicarse reiteradamente la re-
solución termina aplicándose a situaciones muy simples. La soluciones a ciertos problemas se pueden plantear de
manera recursiva. Esto significa por tanto, que su solución se apoya en la solución del mismo problema, pero para
un caso más fácil.
En matemáticas es frecuente encontrar situaciones en los que es posible definir conceptos en términos de sí
mismo. Un ejemplo clásico es la definición de factorial de un número natural. En este caso se tiene
1 si n = 0
n! =
n · (n − 1)! si n > 0
Se trata de un proceso recursivo porque se está utilizando el propio concepto de factorial (aparece (n − 1)!) en la
propia definición. De esta manera se tendría:
n! = n · (n − 1)! = n · (n − 1) · (n − 2)! = · · · = n · (n − 1) · · · 2 · 1
No todos los lenguajes permiten codificar algoritmos recursivos, pero en el caso del C, sí. Para la función
factorial su codificación puede verse en el programa 7.21.
19 fclose(fi);
20
21 return 0;
22 }
23
24 /* Función factorial */
25 int factorial(int x)
26 {
27 if (x == 0)
28 return (1);
J. Sanguino
204 Capítulo – 7. Funciones
29 return (x * factorial(x-1));
30 }
El factorial de 5 es 120
Otro ejemplo interesante es la Sucesión de Fibonacci. Consiste en una sucesión de números naturales que comienza
con los números 1 y 1 y a partir de estos, cada término es la suma de los dos anteriores. Se puede escribir de manera
recursiva de la siguiente forma:
1 si n = 1
F (n) = 1 si n = 2
F (n − 1) + F (n − 2) si n > 2
9 int N;
10
11 FILE *fi;
12 fi = fopen("Datos.dat", "w");
13
14 printf("Introduce el valor de N (>=1) -> ");
15 scanf(" %d", &N);
16 fprintf(fi, " El término %d de Fibonacci es %d", N, fibonacci(N));
17
18
19 fclose(fi);
20
21 return 0;
22 }
23
24 /* Sucesión de Fibonacci */
25 int fibonacci(int x)
26 {
27 if (x == 1 || x == 2)
28 return (1);
29 else
30 return (fibonacci(x-1) + fibonacci(x-2));
31 }
Cualquier problema que puede resolverse de manera recursiva, admite solución mediante un proceso iterativo.
A veces, el procedimiento de pasar un algoritmo recursivo a uno iterativo, no es directo y puede dar lugar a
J. Sanguino
7.9 – Recursividad 205
códigos más complejos de seguir. No obstante, los algoritmos recursivos suelen ser menos eficientes (en tiempo)
que los equivalentes iterativos. Esto es debido, por lo general, a la sobrecarga debida al funcionamiento interno de
recursividad (es necesario definir la pila en donde se almacenan los resultados intermedios) y a la posible solución
recursiva.
El hecho que la recursividad presente cierta sobrecarga (en tiempo de ejecución del algoritmo) frente a la
iteración, no significa que no se deba utilizar, sino que se debe emplear cuando sea apropiado. Cuando el problema
a resolver sea relativamente simple, se recomienda utilizar la solución iterativa ya que, como se ha comentado, es
más eficiente. Sin embargo, las soluciones a ciertos problemas más complejos pueden ser mucho más fáciles si se
utiliza la recursividad. En este caso, la claridad y simplicidad del algoritmo recursivo prevalece frente al tiempo
extra que puede consumir en la ejecución. Un ejemplo de esta situación aparece con el problema de las Torres de
Hanoi.
Sean tres estacas verticales y n discos de distintos radios que pueden insertarse en las estacas
formando torres. Inicialmente los n discos están todos situados en la primera estaca, por orden de-
creciente de radios (veáse figura 7.3). Se trata de pasar los n discos de la primera estaca a la última
siguiendo las reglas:
Para una torre de altura uno, la solución es trivial, como puede verse en la figura 7.4
I C D I C D
La solución para una torre de dos discos, consta de sólo tres pasos, como puede visualizarse en la figura 7.5.
A la vista de las dos situaciones anteriores, cabe la posibilidad de generalizar la solución para n discos. En
efecto, si se supone el problema resuelto para una torre de altura n − 1 discos (es decir, se sabe mover n − 1 discos
de una estaca a otra, utilizando la que queda como auxiliar), el problema de pasar n discos desde la estaca I o
izquierda a la D o derecha, queda resuelto del siguiente modo (véase figura 7.6):
J. Sanguino
206 Capítulo – 7. Funciones
I C D I C D
I C D I C D
I C D I C D
1. Pásense n − 1 discos de la estaca I o izquierda a la estaca C o central, utilizando la estaca D o derecha como
auxiliar.
3. Pásense n − 1 discos de la estaca central o C a la estaca derecha o D, utilizando la estaca izquierda o I como
auxiliar.
Este es el proceso recursivo que se programa en el código 7.23
J. Sanguino
7.9 – Recursividad 207
21 fclose(fi);
22
23 return 0;
24 }
25
26 void T_hanoi(FILE *fo, int N, char ini, char aux, char fin)
27 {
28 if(N == 1)
29 fprintf(fo, " %c -> %c", ini, fin);
30 else{
31 T_hanoi(fo, N-1, ini, fin, aux); /* Mueve n-1 discos al centro */
32 fprintf(fo, "\n %c -> %c\n", ini, fin); /* Mueve el disco mayor a la decha.
*/
33 T_hanoi(fo, N-1, aux, ini, fin); /* Mueve n-1 discos del centro a la decha.
*/
34 }
35
36 return;
37 }
La cuestión clave está en la definición de la función recursiva T_hanoi, que muestra en pantalla los movimientos
para mover N discos de la estaca identificada con la letra I (Izda.) a la estaca D (Dcha.) y que utiliza la estaca C
(Centro), como auxiliar. La posición de los argumentos es muy importante para fijar los movimientos de los discos.
La función tiene como solución simple, cuando la torre está formada por un disco (N = 1). En este caso, basta
realizar un único movimiento: mover la torre de la estaca I a la D. En otro caso, si la torre está formada por más
de un disco, entonces
1. Se deben trasladar los N − 1 discos desde la estaca I a la estaca C. Para ello se utiliza la estaca D, como
auxiliar. Esto se consigue utilizando la función T_hanoi, pero cambiando el orden de los argumentos,
establecidos en la definición. Así pues, en la LÍNEA 29 se llama a la función de la forma
en donde se ha cambiado el orden de los argumentos. Esto quiere decir ahora, que los N − 1 discos se
J. Sanguino
208 Capítulo – 7. Funciones
trasladan desde ini (que contiene a I) a aux (que contiene a C), utilizando a fin (que contiene a D) como
estaca auxiliar.
3. La última etapa consiste en trasladar los N − 1 discos de la estaca del centro (C) a la derecha (D). Esto
se consigue con la sentencia de la LÍNEA 31. En ella se toma la estaca izda. (I) como auxiliar, cambiando
de nuevo, el orden de los argumentos en la función T_hanoi. Por esta razón, primero aparece la variable
aux (que contiene C y es desde donde se van a trasladar los discos), después aparece la variable ini (que
contiene a I que es la estaca que se utiliza como auxiliar) y por último está la variable fin (que contiene D
que es la estaca de destino).
Obsérvese que el número de pasos necesarios para trasladar los N discos de la estaca A a la estaca C será de
2N − 1.
7.10. Problemas
1. Haz un programa en el que se defina la función Presentacion() cuyo objetivo es que escriba en pantalla,
el número de matrícula, el nombre y los apellidos. Mejora esta función para que los mismos datos se escriban
en un fichero de salida.
2. A partir de la función f_potencia ya vista en el código 7.9 (pág. 181) realiza un programa en la que
se defina y utilice una función que calcule la potencia de dos números enteros. Esto es, dados m y n, que
calcule mediante una función, el valor: mn y lo imprima en un fichero.
3. Utiliza la función definida anteriormente para calcular las variaciones con repetición de m elementos toma-
dos de n en n ya planteado en la sección 4.7 de Problemas, pág. 92.
4. Utiliza la función Factorial definida en el código 7.10 (pág. 182), para calcular las variaciones sin
repetición de m elementos tomados de n en n y para determinar el número combinatorio
m m!
=
n n!(m − n)!
conceptos ya vistos en la sección 4.7 de Problemas, pág. 92. Una vez visto el concepto de recursividad,
reescribe la función Factorial para que el cálculo se realice de forma recursiva.
5. Escribe un programa que dado un número m ∈ N, calcule todos los números combinatorios siguientes:
m m m m
, , ,...,
0 1 2 m
y muestre todos ellos en un fichero de salida, en el mismo orden. En este ejercicio y a partir del problema
anterior, es obligado que se defina y utilice una función combinatorio cuyo argumento de entrada sean
dos números positivos y la salida su número combinatorio correcto.
(Propuesto en examen: curso – 2014/15)
a) Se pide que se escriba un programa que dado un número natural m > 1 calcule 2m , SIN utilizar la
función pow(x,y) de la biblioteca de C. Por tanto, se tendrá que definir la función correspondiente.
b) Además el programa debe evaluar el binomio de Newton (esto es, programar el miembro de la derecha
de la igualdad anterior), para comprobar que el cálculo anterior es correcto y mostrarlo en un fichero
de salida. Después deben analizarse si ambos resultados son iguales. Si los son, la salida debe ser
J. Sanguino
7.10 – Problemas 209
J. Sanguino
210 Capítulo – 7. Funciones
10. Se llama número de Armstrong al número que verifica la propiedad de que la suma de cada uno de sus
dígitos elevado a la potencia del número de dígitos, coincide con su valor. Por ejemplo el número 371 es un
número de Armstrong ya que cumple
33 + 73 + 13 = 371
o bien, el número 92727
95 + 25 + 75 + 25 + 75 = 92727
Otros números de Armstrong son: 1, 153, 370, 371, 407, 1634, 8208, 9474, 54748, 92727, 93084, 548834. Se
pide que hagas una función de manera que dado un número natural N , determine si es de Armstrong o no.
A partir de esta función, haz un programa que calcule los números de Armstrong menores de 108 (hay 27 y
menores a 109 , 31) y los imprima en un fichero.
11. Haz una función tal que dada una matriz permita permutar dos filas de la matriz.
12. Haz una función tal que dada una matriz A ∈ Mn×m (R), se obtenga su transpuesta.
(Sugerencia: constrúyase la matriz transpuesta sobre una matriz cuadrada de tamaño máx(n, m)).
13. Dada una matriz A ∈ Mn×m , se dice que uno de sus coeficientes es un punto de silla si es el máximo de su
fila y mínimo de su columna o bien, si es el mínimo de su fila y máximo de su columna. Se pide que hagas
un programa que determine los posibles puntos de silla de una matriz entera dada A ∈ Mn×m (Z). Se deben
definir y utilizar al menos 3 funciones: Maximo, Minimo y PuntoSilla desde la cual, se debe imprimir
la solución. Una ejecución típica del programa debe ser:
Introduce A[1][1] = 1
Introduce A[1][2] = 2
Introduce A[1][3] = 3
Introduce A[2][1] = 5
Introduce A[2][2] = 6
Introduce A[2][3] = 7
Introduce A[3][1] = 4
Introduce A[3][2] = 1
Introduce A[3][3] = 9
J. Sanguino
7.10 – Problemas 211
15. Haz un programa que calcule el m.c.d. de dos números naturales mediante el algoritmo de Euclides, definida
en una función cuya codificación debe estar realizada de manera recursiva.
16. En un curso de álgebra lineal se define la Traza de una matriz A ∈ Mm×n como la suma de las componentes
de la diagonal principal. Esto es
a1, 1 . . . a1, n
k
. ..
A = .. .. =⇒ T r(A) =
X
ai, i donde k = mı́n(n, m)
. .
i=1
am, 1 . . . am, n
En el caso de matrices del mismo tamaño, el concepto de traza se puede asociar al de un operador lineal.
En este ejercicio se pide que dada una matriz simétrica aleatoria A ∈ Mn×n calcules su componente
desviatoria esto es
1
AD = A − T raza(A)I
n
donde I es la matriz identidad. El concepto de componente desviatoria es una generalización matemática de
la componente desviatoria del tensor de tensiones, que indica cuánto se aparta el estado tensional un sólido,
respecto de un estado hidrostático o isotrópico.
Para realizar el ejercicio se debe:
a) definir una función llamada MatSimAl que obtenga la matriz simétrica aleatoria, en la cual se pida los
extremos del intervalo en donde estarán los números aleatorios, así como la semilla para generarlos.
El proceso para generar esta matriz es muy sencillo. No obstante, se puede seguir la siguiente sugeren-
cia
Como argumento de la función debe estar el puntero a una matriz cuadrada. A continuación, en la parte triangular
superior de la matriz se situarán los números aleatorios (el proceso es similar al realizado en clase de prácticas,
para imprimir figuras triangulares de ejercicios planteados en el capítulo de Sentencias de Control). Es decir,
Una vez asignados estos valores, se rellenan las componentes de la parte triangular inferior con la regla:
aj, i = ai, j
J. Sanguino
212 Capítulo – 7. Funciones
El intervalo es [-1, 5]
La semilla es -1
17. En un curso de Álgebra lineal se define la Traza de una matriz A ∈ Mm×n como la suma de las compo-
nentes de la diagonal principal. Esto es
a1, 1 . . . a1, n
k
.. ..
A= . . .
X
. . =⇒ T r(A) = ai, i donde k = mı́n(n, m)
i=1
am, 1 . . . am, n
En el caso de matrices del mismo tamaño, el concepto de traza se puede asociar al de un operador lineal.
En este ejercicio se pide que dado un vector aleatorio real v ∈ Rn , se calcule el valor de la traza del
producto diádico del vector v por sí mismo. Esto es
T r(v ⊗ v)
a) definir una función llamada VectorAleatorio que genere el vector aleatorio v, en la cual se pida
los extremos del intervalo en donde estarán los números aleatorios, así como la semilla para generarlos.
b) definir una función llamada ProdTensorial, que a partir del vector v se genere la matriz corres-
pondiente al producto diádico o tensorial del vector v por sí mismo.
c) definir una función llamada Traza que determine la traza de una matriz cualquiera y sea utilizada
para obtener el resultado pedido. Su aplicación debe hacerse desde el programa principal.
Una ejecución típica del programa con salida en pantalla sería:
J. Sanguino
7.10 – Problemas 213
El intervalo es [-1, 2]
La semilla es -3
El vector aletorio es de tamaño 5 con valores:
-0.99744; -0.24540; -0.96219; 1.61400; -0.02228;
En este ejercicio se pide que dado un polinomio con coeficientes enteros P (x) obtengas la sucesión que
genera el método para aproximar y(1) ≈ yM .
Para realizar el ejercicio se debe:
a) introducir desde teclado el nombre de un fichero en el que se escribirán los datos de salida.
b) introducir el número de pasos, M , para generar la sucesión que aproxima y(1), así como el grado del
polinomio P (x), n.
c) definir una función llamada GenPol que obtenga los coeficientes enteros aleatorios del polinomio
P (x), en la cual se pida los extremos del intervalo en donde estarán los números aleatorios, así como
la semilla para generarlos. Los coeficientes del polinomio resultante serán reales de doble precisión,
se imprimirán por pantalla y en el fichero desde el programa principal.
d) definir una función llamada Pol que calcule, y devuelva, el valor del polinomio P (x) para un valor
real cualquiera x, que deberá ser real de doble precisión.
e) definir una función llamada Euler que genere la sucesión de valores yi i = 0, 1, . . . , M que será
devuelta al programa principal en un vector.
f ) por último, desde el programa principal debe imprimirse en el fichero y por pantalla la sucesión de
puntos (xi , yi ) i = 0, 1, 2, . . . , M , siendo yM ≈ y(1)
J. Sanguino
214 Capítulo – 7. Funciones
***************************
Sucesión generada:
Salida de los puntos x(i) y sus aproximaciones y(i)
i x(i) y(i)
0 0.0000 1.0000
1 0.1000 1.0368
2 0.2000 1.0758
3 0.3000 1.1193
4 0.4000 1.1690
5 0.5000 1.2259
6 0.6000 1.2899
7 0.7000 1.3598
8 0.8000 1.4336
9 0.9000 1.5091
10 1.0000 1.5842
19. Se pide que realices un programa que determine el vector y ∈ Rn , resultado del producto de la matriz
A ∈ Mn×n con el vector x ∈ Rn . Matricialmente:
y1 a1, 1 . . . a1, n x
1 n
.. .. . ..
= . . . ⇐⇒ y =
X
ai, j xj con i = 1, 2, . . . , n
.
. . . . i
j=1
yn an, 1 . . . an, n xn
a) definir una función llamada VecMatInt en la que se establezcan las componentes del vector x y de
la matriz cuadrada A. Si la introducción se realiza mediante números aleatorios, en esta función se
debe introducir el intervalo en donde estarán los números aleatorios, así como la semilla. Si se opta
por introducir las componentes por teclado, deberá programarse de manera clara y ordenada para que
el usuario sepa qué componentes del vector x y de la matriz cuadrada A está introduciendo.
b) definir una función llamada Prod en la que se realice el producto pedido.
c) imprimir, desde el programa principal, el vector y resultado del producto obtenido en la función Prod.
J. Sanguino
7.10 – Problemas 215
El intervalo es [-2, 5]
La semilla es -1
El vector aletorio es de tamaño 3 con valores:
1.00; 1.00; 4.00;
20. Dada una matriz A ∈ Mm×n se llama norma infinito de dicha matriz, al número real:
n
X
kAk∞ = máx |ai, j |
16i6m
j=1
En este ejercicio se pide que dada una matriz aleatoria calcules su norma infinito.
Para realizar el ejercicio se debe:
a) ejecutar desde el programa principal una función llamada MatrizAleatoria que genere, como
coeficientes de la matriz, números aleatorios reales de más de 10 cifras significativas. En esta función
se debe pedir al usuario, los extremos del intervalo (números reales, en cualquier orden), así como la
semilla para generarlos.
b) ejecutar desde el programa principal una función llamada NormaInf que deberá devolver al programa
principal el valor de la norma infinito. La matriz será un argumento de la función.
c) emplear una función real llamada Absoluto, que deberá devolver el valor absoluto de cualquier
número real. Esta función debe ser definida por el alumno, utilizando el operador condicional, tal y
como se ha hecho en clase infinidad de veces.
d) imprimir en el fichero de salida, desde el programa principal, la matriz aleatoria junto con su norma.
e) introducir desde el teclado el nombre del fichero de salida.
f ) imprimir los datos del alumno (nombre, apellidos y número de matrícula) al comienzo del fichero de
salida.
J. Sanguino
216 Capítulo – 7. Funciones
S n−1 = {x ∈ Rn / kxk2 = 1}
mediante la función
x
F (x) = (7.2)
kxk
Se pide que realices un programa que dado un vector aleatorio de números reales de dimensión n, determine
su proyección sobre S n−1 , mediante la función (7.2).
J. Sanguino
7.10 – Problemas 217
b) llamar desde el programa principal una función denominada Proyec que deberá devolver al programa
principal el vector proyección sobre S n−1 . La norma que debe utilizarse para construir la proyección
a partir de la función (7.2), debe ser la norma uno:
n
X
kxk1 = |xi |
i=1
c) utilizar una función llamada Norma_Uno que calcule dicha norma de un vector real.
d) emplear una función real llamada Absoluto, que deberá devolver el valor absoluto de cualquier
número real. Esta función debe ser definida por el alumno, utilizando el operador condicional, tal y
como se ha hecho en clase infinidad de veces.
e) imprimir en el fichero de salida, desde el programa principal, el vector aleatorio inicial junto con el
vector proyección.
f ) introducir desde el teclado el nombre del fichero de salida.
g) imprimir los datos del alumno (nombre, apellidos y número de matrícula) al comienzo del fichero de
salida.
en la pantalla
Introduce el nombre del fichero -> Salida.dat
en el fichero
J. Sanguino
218 Capítulo – 7. Funciones
22. Dada una matriz cuadrada A ∈ Mn×n se denominan términos triangulares superiores a las siguientes
componentes de la matriz:
16i6n
ai, j con
i6j6n
Se pide que realices un programa que dada una matriz cuadrada aleatoria de números reales, se calcule el
promedio de los términos triangulares superiores (esto es, la suma de los términos dividido por la cantidad
de ellos).
Para realizar el ejercicio se debe:
a) llamar desde el programa principal una función denominada MatrizAleatoria que genere, como
coeficientes de la matriz, números aleatorios reales de más de 10 cifras significativas. Dentro de esta
función se debe pedir al usuario, los extremos del intervalo (números reales, en cualquier orden), así
como la semilla para generarlos. Estos datos deberán escribirse en un fichero de salida.
b) llamar desde el programa principal una función denominada Promedio que deberá devolver al pro-
grama principal el valor del promedio de los términos triangulares superiores. La matriz será un argu-
mento de la función.
c) disponer correctamente la matriz aleatoria generada, junto con el valor promedio de los términos trian-
gulares inferiores, que deberá imprimirse en el fichero de salida, desde el programa principal.
d) introducir el nombre del fichero de salida por el teclado.
e) escribir los datos de alumno (nombre, apellidos y número de matrícula) al comienzo del fichero de
salida.
en la pantalla
Introduce el nombre del fichero -> Salida.dat
J. Sanguino
7.10 – Problemas 219
23. Dada una matriz cuadrada A ∈ Mn×n se dice que es diagonal dominante si
n
X
|ai, i | > |ai, j | con i = 1, . . . , n
j=1
i6=j
Se pide que realices un programa que dada una matriz cuadrada aleatoria de números reales, determine si
una matriz cuadrada es diagonal dominante o no.
Para realizar el ejercicio se debe:
a) llamar desde el programa principal a una función denominada MatrizAleatoria que genere, como
coeficientes de la matriz, números aleatorios reales de más de 10 cifras significativas. Dentro de esta
función se debe pedir al usuario, los extremos del intervalo (números reales, en cualquier orden), así
como la semilla para generarlos. Estos datos deberán escribirse en un fichero de salida.
b) llamar desde el programa principal una función denominada Dominante que deberá devolver al
programa principal una señal de si es diagonal dominante la matriz o no. La matriz será un argumento
de la función.
c) emplear una función real llamada Absoluto, que deberá devolver el valor absoluto de cualquier
número real. Esta función debe ser definida por el alumno, utilizando el operador condicional, tal y
como se ha hecho en clase infinidad de veces.
d) disponer correctamente la matriz aleatoria generada, junto con el resultado de si es diagonal dominiante
o no que se imprimirá en el fichero de salida, desde el programa principal.
e) introducir el nombre del fichero de salida por el teclado.
f ) los datos del alumno (nombre, apellidos y número de matrícula) deberán aparecer al comienzo del
fichero de salida.
en la pantalla
Introduce el nombre del fichero -> Salida.dat
en el fichero
J. Sanguino
220 Capítulo – 7. Funciones
24. Se pide que realices un programa que calcule el producto escalar del vector x ∈ Rn con la diagonal principal
de la matriz cuadrada A ∈ Mn×n . Esto es
x1 a1, 1 . . . a1, n
n
.. .. ..
[x] = . si A = . . .
X
xi · ai, i
. . =⇒ Prod =
i=1
xn an, 1 . . . an, n
a) definir una función llamada VecMatInt en la que se establezcan las componentes del vector x y de la
matriz cuadrada A, que deben ser reales de más de 10 cifras significativas. Si la introducción se realiza
mediante números aleatorios, en esta función se debe introducir el intervalo (de extremos reales) en
donde estarán los números aleatorios, así como la semilla. Si se opta por introducir las componentes
por teclado, deberá programarse de manera clara y ordenada para que el usuario sepa qué componentes
del vector x y de la matriz cuadrada A está introduciendo.
Tanto el vector como la matriz introducida deben imprimirse en el fichero de salida, desde el programa
principal.
b) definir una función llamada Prod en la que se realice el producto pedido.
c) imprimir en el fichero de salida, desde el programa principal, la solución del resultado obtenido en la
función Prod.
d) disponer correctamente, en el fichero de salida, la matriz aleatoria generada, junto con el vector gene-
rado.
e) introducir el nombre del fichero de salida por el teclado.
f ) escribir los datos de alumno (nombre, apellidos y número de matrícula) al comienzo del fichero de
salida, mediante una función.
Una ejecución típica del programa con salida en pantalla sería:
J. Sanguino
7.10 – Problemas 221
25. Dada una matriz A ∈ Mm×n se pide que construyas un vector cuyas componentes deben ser la suma del
valor absoluto de las filas, en las respectivas columnas. Esto es, si
a1, 1 . . . a1, n
m m
" #
. ..
A = .. .. entonces se pide el vector: v=
X
|ai, 1 |, · · · ,
X
|ai, n |
. .
i=1 i=1
am, 1 . . . am, n
a) ejecutar desde el programa principal una función llamada Matriz_a que genere, como coeficientes
de la matriz, números aleatorios enteros. En esta función se debe pedir al usuario, los extremos del in-
tervalo (números enteros, en cualquier orden), así como la semilla para generarlos. Estos datos deberán
escribirse en un fichero de salida.
b) la matriz aleatoria generada debe escribirse en el fichero de salida, desde el programa principal.
c) ejecutar desde el programa principal una función llamada Suma_Filas siendo uno de los argumen-
tos, la matriz generada anteriormente y que deberá devolver al programa principal el vector pedido.
Desde el programa principal deberá escribirse en el fichero de salida este vector obtenido.
d) emplear una función entera llamada Absoluto, que deberá devolver el valor absoluto de cualquier
número entero. Esta función debe ser definida por el alumno, utilizando el operador condicional, tal y
como se ha hecho en clase infinidad de veces.
e) introducir desde el teclado el nombre del fichero de salida.
f ) imprimir los datos del alumno (nombre, apellidos y número de matrícula) al comienzo del fichero de
salida.
En la pantalla:
Introduce el nombre del fichero -> Salida.dat
J. Sanguino
222 Capítulo – 7. Funciones
El intervalo es [-2, 2]
La semilla es 1
La matriz introducida es:
A[1][1] = -1; A[1][2] = 0; A[1][3] = 2; A[1][4] = -2; A[1][5] = 2;
A[2][1] = 2; A[2][2] = 1; A[2][3] = 1; A[2][4] = 0; A[2][5] = 2;
A[3][1] = -2; A[3][2] = -2; A[3][3] = -1; A[3][4] = 0; A[3][5] = -1;
26. Dada una matriz A ∈ Mm×n se pide que hagas un código en C, que calcule el producto escalar obtenido al
multiplicar la primera y última columna de dicha matriz, en valor absoluto. Esto es,
m
X
K= |ai, 1 | · |ai, n |
i=1
a) llamar desde el programa principal a una función denominada MatrizAleatoria que genere, como
coeficientes de la matriz, números aleatorios reales de más de 10 cifras significativas. Dentro de esta
función se debe pedir al usuario, los extremos del intervalo (números reales, en cualquier orden), así
como la semilla para generarlos. Estos datos deberán escribirse en un fichero de salida.
b) llamar desde el programa principal una función denominada Escalar que deberá devolver al progra-
ma principal el valor de dicho producto escalar. La matriz será un argumento de la función.
c) emplear una función real llamada Absoluto, que deberá devolver el valor absoluto de cualquier
número real. Esta función debe ser definida por el alumno, utilizando el operador condicional, tal y
como se ha hecho en clase infinidad de veces.
d) disponer correctamente la matriz aleatoria generada, junto con el resultado que se imprimirá en el
fichero de salida, desde el programa principal.
e) introducir el nombre del fichero de salida por el teclado.
f ) los datos del alumno (nombre, apellidos y número de matrícula) deberán aparecer al comienzo del
fichero de salida.
en la pantalla
J. Sanguino
7.10 – Problemas 223
en el fichero
27. Dada una matriz A ∈ Mm×n se pide que realices un código en C, que calcule el producto de los coeficientes,
en valor absoluto, de la diagonal principal. Esto es,
a1, 1 . . . a1, n
. ..
A = .. .. entonces se pide: P = Πki=1 |ai, i |
. .
am, 1 . . . am, n
a) llamar desde el programa principal a una función denominada MatrizAleatoria que genere, como
coeficientes de la matriz, números aleatorios reales de más de 10 cifras significativas. Dentro de esta
función se debe pedir al usuario, los extremos del intervalo (números reales, en cualquier orden), así
como la semilla para generarlos. Estos datos deberán escribirse en un fichero de salida.
b) llamar desde el programa principal una función denominada Prod_Diagonal que deberá devolver
al programa principal el valor de dicho producto. La matriz será un argumento de la función.
J. Sanguino
224 Capítulo – 7. Funciones
c) emplear una función real llamada Absoluto, que deberá devolver el valor absoluto de cualquier
número real. Esta función debe ser definida por el alumno, utilizando el operador condicional, tal y
como se ha hecho en clase infinidad de veces.
d) disponer correctamente la matriz aleatoria generada, junto con el resultado que se imprimirá en el
fichero de salida, desde el programa principal.
e) introducir el nombre del fichero de salida por el teclado.
f ) los datos del alumno (nombre, apellidos y número de matrícula) deberán aparecer al comienzo del
fichero de salida.
en la pantalla
Introduce los extremos del intervalo (en cualquier orden) -> 5.5 -3.5
en el fichero
28. Se pide que realices un programa que calcule el coseno del ángulo que forman dos vectores x, y ∈ Rn . Esto
es
x·y
cos θ =
kxkkyk
J. Sanguino
7.10 – Problemas 225
donde
n
X
x · y = x1 y1 + · · · + xn yn = x i yi
i=1
√
kxk = x·x
a) definir una función llamada VecAlea en la que se establezcan las componentes de un vector v
(v ∈ Rn ), que deben ser reales de más de 10 cifras significativas. Si la introducción se realiza mediante
números aleatorios, en esta función se debe introducir el intervalo (de extremos reales) en donde esta-
rán los números aleatorios, así como la semilla. Si se opta por introducir las componentes por teclado,
deberá programarse de manera clara y ordenada para que el usuario sepa qué componentes del vector
v está introduciendo.
Los vectores introducidos deben imprimirse en el fichero de salida, desde el programa principal.
b) definir una función llamada Prod en la que se realice el producto escalar de dos vectores.
c) definir una función llamada Norma que calcule la norma de un vector.
d) definir una función llamada CosAng, que calcule el coseno del ángulo, cuyo valor deberá imprimirse
desde el programa principal. En esta función deben utilizarse las funciones Prod y Norma definidas
anteriormente.
e) disponer correctamente, en el fichero de salida, los vectores generados, junto con la solución obtenida.
f ) introducir el nombre del fichero de salida por el teclado.
g) escribir los datos de alumno (nombre, apellidos y número de matrícula) al comienzo del fichero de
salida, mediante una función.
Una ejecución típica del programa con salida en pantalla sería:
J. Sanguino
226 Capítulo – 7. Funciones
V1[1] = -3.49; V1[2] = 0.45; V1[3] = -2.15; V1[4] = 2.16; V1[5] = 0.60;
A partir de esta definición se puede generalizar el concepto de matriz cuadrada bidiagonal superior y
considerar matrices de este tipo de orden n.
Se pide que realices un programa en C que genere una matriz cuadrada aleatoria entera bidiagonal superior
y posteriormente calcule la norma infinito del vector que contenga la subdiagonal superior de dicha matriz.
Se recuerda que la norma infinito de un vector viene dada por la expresión:
Para ello:
a) se debe definir una función externa llamada Matriz_BiSup, que devolverá al programa principal, la
matriz cuadrada aleatoria bidiagonal superior desde donde se imprimirá en el fichero de salida. En esta
función debe pedirse al usuario, tanto el intervalo de generación de números aleatorios (de extremos
enteros), como la semilla.
Se recuerda que para generar números enteros aleatorios en el intervalo [M, N ] debe aplicarse la instrucción:
numero = rand() % (N - M + 1) + M;
b) se debe codificar una función externa denominada Vect_Sup, que se llamará desde el programa
principal. Uno de sus argumentos será la matriz generada anteriormente. Debe devolver al programa
principal un vector que contenga la subdiagonal superior y valor de su norma infinito, desde donde se
imprimirán en el fichero de salida.
c) se debe definir una función externa llamada Norma_Inf para calcular la norma infinito de un vector
(tal y como se ha explicado en clase).
d) el valor absoluto se determinará a través de una función externa llamada Absoluto con el operador
condicional y NO debe utilizarse la función abs de la biblioteca estándar.
e) debe crearse una función externa llamada Presentacion que imprima en el fichero de salida, los
datos del alumno: nombre, apellidos y número de matrícula. Así mismo, se debe poder introducir por
el teclado, el nombre del fichero de salida.
J. Sanguino
7.10 – Problemas 227
En la pantalla:
Introduce el nombre del fichero -> Salida.dat
El intervalo es [-5, 9]
La semilla es -1
La matriz Bidiagonal Superior es:
Mat[1][1] = 0; Mat[1][2] = -2; Mat[1][3] = 0; Mat[1][4] = 0;
Mat[2][1] = 0; Mat[2][2] = 4; Mat[2][3] = 5; Mat[2][4] = 0;
Mat[3][1] = 0; Mat[3][2] = 0; Mat[3][3] = 9; Mat[3][4] = -2;
Mat[4][1] = 0; Mat[4][2] = 0; Mat[4][3] = 0; Mat[4][4] = 6;
Se pide que realices un programa en C que genere una matriz cuadrada aleatoria real triangular inferior y
posteriormente se calcule la matriz antisimétrica asociada a ella (i.e. R = [ai, j ] con ai, j = −aj, i ), junto
con la norma uno de dicha matriz antisimétrica. Los números reales de la matriz deben tener al menos, 10
cifras significativas.
La matriz antisimétrica asociada a la matriz triangular inferior se construye a partir de sustituir los términos
nulos ai, j (en las posiciones i < j), por los respectivos términos −aj, i (siendo nulos los términos de la
diagonal principal). Así mismo, se recuerda que la norma uno de una matriz viene definida mediante la
expresión:
X n
kAk1 = máx |ai, j |
16j6n
i=1
J. Sanguino
228 Capítulo – 7. Funciones
b) se debe codificar una función externa denominada Matriz_Anti, que se llamará desde el programa
principal. Uno de sus argumentos será la matriz generada anteriormente. Debe devolver al programa
principal la matriz antisimétrica y valor de su norma uno, desde donde se imprimirán en el fichero de
salida.
c) se debe definir una función externa llamada Norma_1 para calcular la norma uno de una matriz (tal y
como se ha explicado en clase).
d) el valor absoluto se determinará a través de una función externa llamada Absoluto con el operador
condicional y NO debe utilizarse la función abs de la biblioteca estándar.
e) debe crearse una función externa llamada Presentacion que imprima en el fichero de salida, los
datos del alumno: nombre, apellidos y número de matrícula. Así mismo, se debe poder introducir por
el teclado, el nombre del fichero de salida.
En la pantalla:
Introduce el nombre del fichero -> Salida.dat
J. Sanguino
7.10 – Problemas 229
31. La búsqueda de un proceso para ordenar los coeficientes de un vector resulta fundamental para ciertos
resultados. En el siguiente problema se trata que codifiques un programa en C, que calcule la mediana de
una serie de valores contenidos en un vector. Para ello es necesario que los valores de dicho vector estén
previamente ordenados. Se plantea el siguiente pseudocódigo de ordenación:
El vector ordenado es
v[1] v[2]....v[n]
X1 , X2 , . . . , XN
y que se hayan almacenados en un vector v. Se pide, que una vez ordenados de menor a mayor con el
algoritmo anterior, calcules la mediana utilizando la siguiente expresión:
X[ N ]+1 si N/2 6∈ N
2
md =
1 XN + XN
si N/2 ∈ N
2 2 2
+1
a) definir una función llamada Vector_a siendo uno de sus argumentos un vector v (v ∈ Rn ) (de al
menos 10 cifras significativas) que generará las componentes de dicho vector. Estas se imprimirán en el
fichero de salida desde la función principal. En esta función debe pedirse al usuario, tanto el intervalo
de generación de números aleatorios (de extremos enteros), como la semilla.
Se recuerda que para generar números reales aleatorios en el intervalo [M, N ] debe aplicarse la instrucción:
b) definir una función llamada Ordena en la que se codifique el algoritmo de ordenación anterior y
que será ejecutada desde la función principal. El vector ordenado deberá imprimirse desde la función
principal.
c) definir una función llamada Mediana, que devuelva a la función principal la mediana, según el pro-
cedimiento planteado. El valor de la mediana se imprimirá desde la función principal.
d) disponer correctamente, en el fichero de salida, el vector inicial, el vector ordenado y el valor de la
mediana, obtenido.
e) introducir el nombre del fichero de salida por el teclado.
J. Sanguino
230 Capítulo – 7. Funciones
El intervalo es [0, 5]
La semilla es 1
El vector de 8 componentes es:
V[1] = 0.01; V[2] = 2.82; V[3] = 0.97; V[4] = 4.04; V[5] = 2.93; V[6] = 2.40;
V[7] = 1.75; V[8] = 4.48;
La mediana es 2.61
A partir de esta definición se puede generalizar el concepto de matriz cuadrada tridiagonal y considerar
matrices de este tipo de tamaño n.
Se pide que realices un programa en C que genere una matriz cuadrada aleatoria real tridiagonal y poste-
riormente calcules el producto escalar de los vectores constituidos por las diagonales que quedan por encima
y por debajo de la diagonal principal de dicha matriz. Los números reales de la matriz deben tener al menos,
10 cifras significativas y se recuerda que el producto escalar de dos vectores viene dado por:
k
X
x·y = xi yi
i=1
Para ello:
a) se debe definir una función llamada Matriz_Tridiagonal, que generará la matriz cuadrada alea-
toria tridiagonal. Esta matriz debe imprimirse en el fichero de salida desde la función principal. En la
función Matriz_Tridiagonal debe pedirse al usuario, tanto el intervalo de generación de núme-
ros aleatorios (de extremos enteros), como la semilla.
Se recuerda que para generar números reales aleatorios en el intervalo [M, N ] debe aplicarse la instrucción:
J. Sanguino
7.10 – Problemas 231
b) se debe codificar una función denominada Vect_Diag que permita obtener la diagonal principal de
la matriz. Uno de sus argumentos será la matriz generada anteriormente. El vector que contenga la
diagonal principal debe imprimirse en el fichero de salida desde la función principal.
c) se debe codificar una función denominada Prod_Esc, que extraiga las diagonales inmediatamente
superior e inferior de la diagonal principal y que calcule el producto escalar de ambos vectores. Uno de
sus argumentos será la matriz generada anteriormente. Este valor se imprimirá en el fichero de salida
desde la función principal.
En la pantalla:
En el fichero:
J. Sanguino
232 Capítulo – 7. Funciones
33. Una matriz cuadrada circulante de tamaño 5, generado por el vector a = [a1 , a2 , a3 , a4 , a5 ] es de la forma
a1 a5 a4 a3 a2
a2 a1 a5 a4 a3
ai−j+1 si i > j
C5 (a) = a3 con algoritmo C (a) = con i, j = 1, 2, . . . n
a2 a1 a5 a4 5 i, j
a si i < j
(n+1)+i−j
a4 a3 a2 a1 a5
a5 a4 a3 a2 a1
Es decir, cada columna se obtiene de la anterior al hacer un desplazamiento cíclico hacia abajo. Así pues, la
matriz se determina completamente por la primera columna.
Se pide que realices un programa en C que genere una matriz cuadrada aleatoria real circulante y poste-
riormente se calcule la norma uno de dicha matriz. Los números reales de la matriz deben tener al menos,
10 cifras significativas y se recuerda que la norma uno de una matriz cuadrada A ∈ Mn (R) viene definida
mediante la expresión:
Xn
kAk1 = máx |ai, j |
16j6n
i=1
a) se debe definir una función llamada Vector_a, que generará el vector aleatorio que permitirá crear
la matriz circulante. Este vector se imprimirá en el fichero de salida desde la función principal. En la
función Vector_a debe pedirse al usuario, tanto el intervalo de generación de números aleatorios (de
extremos enteros), como la semilla.
Se recuerda que para generar números reales aleatorios en el intervalo [M, N ] debe aplicarse la instrucción:
b) se debe codificar una función denominada Matriz_Circulante. Uno de sus argumentos será el
vector generado anteriormente a partir del cual se creará la matriz circulante, tal y como se ha ex-
puesto en el enunciado. En esta misma función se calculará la norma 1 de la matriz, utilizando la
función planteada en el apartado (3) de este enunciado. Tanto la matriz circulante como su norma debe
imprimirse en el fichero de salida desde la función principal.
c) la norma 1 se calculará utilizando una función llamada Norma_1.
d) el valor absoluto se determinará a través de una función llamada Absoluto y NO deben utilizarse
funciones predefinidas de las bibliotecas.
e) se debe introducir el nombre del fichero de salida por el teclado.
En la pantalla:
Introduce el nombre del fichero -> Salida.dat
J. Sanguino
7.10 – Problemas 233
En el fichero:
Apellidos: Sldielsoe Trordx
Nombre: Uldowds
Número de Matrícula: 555555
***************************
El intervalo es [1, 10]
La semilla es 1
El vector generado es:
V[1] = 1.01; V[2] = 6.07; V[3] = 2.74; V[4] = 8.28; V[5] = 6.27;
J. Sanguino
APÉNDICE A
Iniciación a Dev-C++
A.1. Introducción
Para realizar y ejecutar los programas, en este curso de Introducción a la programación en C, se utiliza el
entorno D EV–C++ 5.11–TDM-GCC 4.9.2 que se puede descargar gratuitamente de
http://orwelldevcpp.blogspot.com.es/
D EV–C++ utiliza como compilador una adaptación a Windows del GCC, llamado TDM-GCC 4.9.2. Con esta ver-
sión del compilador se pueden emplear novedades de los estándares C99 y C11. Por otra parte, este entorno tiene
la ventaja que funciona correctamente en W INDOWS 7 y W INDOWS 8.1 siendo capaz de ajustarse a la naturaleza
de 32 o 64 bits del propio sistema sistema operativo.
En este capítulo se trata de enseñar a cómo se puede realizar un programa fuente, compilarlo y ejecutarlo,
dentro de este entorno de programación.
A.2.2. Creación
Una vez, que se tiene instalado el entorno D EV–C++, con el puntero del ratón se hace click dos veces, en
el icono del entorno D EV–C++ que debe estar sobre el escritorio de W INDOWS. Una vez realizado, aparece una
pantalla como muestra la figura A.1(a) A continuación se selecciona Archivo y dentro del submenú Nuevo se opta
por Código fuente, tal y como muestra la figura A.1(b).
En la pestaña superior aparece SinNombre1 y ya se puede escribir texto de C, tal y como muestra la figu-
ra A.2(a) Durante el proceso de escritura se puede ir salvando el fichero, para no perder lo escrito. Para ello se
recurre al icono del disco tal y como muestra la figura A.2(b). En el momento en que se hace click con el puntero
235
236 Apéndice – A. Iniciación a Dev-C++
Salvar o Guardar
del ratón, se abre un menú desplegable como muestra el gráfico A.3(a). Hay que seleccionar la opción: C source
files (*.c). El entorno D EV-C++ toma por defecto ficheros fuente para el C++. Puesto que nos interesa fi-
cheros fuente para el C, hay que indicarlo explícitamente. Una vez seleccionada esta opción, ya se puede teclear el
nombre del fichero, tal y como muestra la figura A.3(b). En el momento que se seleccione Guardar ya aparece
A.2.3. Compilación
Una vez terminado el programa, como se observa en la figura A.4(a), se opta por compilar el programa. Para
ello se utilizan los iconos que se muestran en la figura A.4(b). Se puede utilizar el de compilar o bien el de compilar
y ejecutar.
J. Sanguino
A.2 – Creación, compilación y ejecución de un fichero fuente 237
Ejecución
Compilación Compilación
y Ejecución
Una vez seleccionado el icono de compilación, se compila el fichero fuente y se obtiene la pantalla que muestra
la figura A.5(a). Esto indica que hay un error. En este caso, no se ha puesto el ; al final de la sentencia. Una vez
corregido, se debe volver a compilar y se obtiene ya el proceso sin errores, como muestra la figura A.5(b).
A.2.4. Ejecución
Una vez que la compilación ha sido exitosa, se procede a la ejecución, seleccionando el icono de ejecución,
visto en la figura A.4(a). Una vez seleccionado, aparece la pantalla que muestra la figura A.6.
Es importante señalar, que para que una modificación en el fichero fuente tenga su reflejo en la ejecución del
programa es indispensable, la compilación previa del código fuente una vez realizada dicha modificación.
J. Sanguino
238 Apéndice – A. Iniciación a Dev-C++
J. Sanguino
APÉNDICE B
B.1. Polígonos
B.1.1. Introducción
Un polígono puede definirse como una región conexa del plano, acotada por una sucesión de segmentos rectos
(véase figura B.1). Los segmentos que limitan la región plana son llamados lados y los puntos en los que se unen
los lados, vértices. Los lados consecutivos forman una curva cerrada suave a trozos que se llama frontera, borde
o contorno del polígono1 .
Si el contorno del polígono no se interseca a sí mismo, se dice que es un polígono simple. Además, se dice que
el polígono está positivamente orientado si al recorrer su contorno, la región que encierra queda a la izquierda (es
la llamada regla del sacacorchos, véase figura B.2).
1
A veces, el polígono hace referencia exclusivamente al contorno, no al interior. En este documento no se seguirá este criterio. Se
llamará polígono al contorno, junto a su interior.
239
240 Apéndice – B. Un poco de Geometría Computacional
Teorema B.1.1 (Teorema de Green) Sea D una región con frontera ∂D cerrada simple, suave a trozos y orien-
tada positivamente. Si M (x, y), N (x, y), ∂M/∂y(x, y), ∂N/∂x (x,y) son continuas en un conjunto abierto que
contiene a D, entonces Z ZZ
∂N ∂M
M dx + N dy = − dxdy
∂D D ∂x ∂y
Este teorema, se puede aplicar para calcular el área de la región D. En efecto, si se define
y x
M (x, y) = − y N (x, y) =
2 2
entonces por el teorema de Green,
ZZ Z
1
dxdy = Área(D) = x dy − y dx
D 2 ∂D
La idea es aplicar este resultado para determinar el área del polígono P . Supongamos que se tiene un polígono
P cuyo contorno viene determinado por los vértices: [p1 , . . . , pn ] (se ha de entender que p1 es el vértice siguiente
a pn )
∂P = p1 p2 ∪ p2 p3 ∪ . . . ∪ pn−1 pn ∪ pn p1
positivamente orientado. Entonces,
Z n Z
1 1X
Área(P) = x dy − y dx = x dy − y dx con pn+1 = p1
2 ∂P 2 pi pi+1
i=1
Para ello, se parametriza el segmento pi pi+1 , de manera que el contorno del polígono P se mantenga positivamente
orientado. Suponiendo que pi = (xi , yi ) y pi+1 = (xi+1 , yi+1 ) entonces
x(t) = xi + t(xi+1 − xi )
σ(t) = con t ∈ [0, 1]
y(t) = y + t(y −y )
i i+1 i
por tanto
σ 0 (t) = (xi+1 − xi , yi+1 − yi )
así pues, la integral de línea queda:
Z Z 1
x dy − y dx = x(t) · y 0 (t) − y(t) · x0 (t) dt
pi pi+1 0
Z 1 xi yi
= −yi (xi+1 − xi ) + xi (yi+1 − yi ) dt = xi yi+1 − yi xi+1 =
0 xi+1 yi+1
De esta forma:
n n
1 X xi yi
Z
1X
Área(P) = x dy − y dx = con pn+1 = p1
2 2
i=1 pi pi+1 i=1 xi+1 yi+1
J. Sanguino
B.1 – Polígonos 241
Polı́gono convexo
Polı́gono NO convexo
xi yi 1
∆(pi , pi+1 , pi+2 ) = xi+1 yi+1 1
xi+2 yi+2 1
= (xi+1 − xi ) · (yi+2 − yi ) − (xi+2 − xi ) · (yi+1 − yi )
(xi+1 − xi ) (yi+1 − yi )
=
(xi+2 − xi ) (yi+2 − yi )
1. xi 6 x0 6 xi+1 o xi+1 6 x0 6 xi
Si se cumple alguna de las dos opciones del primer punto y el segundo, entonces se produce una intersección de la
semirecta con el polígono (véase figura B.4).
J. Sanguino
242 Apéndice – B. Un poco de Geometría Computacional
J. Sanguino
Bibliografía
[1] Bruce, E. Thinking in C++, Volume 1 (2nd. Edition). Prentice Hall (2000).
[2] Bryant, R. E. y O’Hallaron, D. R. Computer systems: a programmer’s perspective (Second Edition), tomo 2.
Prentice Hall Upper Saddle River (2011).
[4] Deitel, P. y Deitel, H. C for programmers (with a introduction to C11). Deitel/Prentice Hall (2013).
[5] Floyd, T. Fundamentos de sistemas digitales (novena edición). Pearson/Prentice Hall (2006).
[7] Horton, I. Beginning C. From novice to professional. (Fourth Edition). Apress (2013).
[8] Infante del Río, J.-A. y Rey Cabezas, J. M. Métodos Numéricos: Teoría, problemas y prácticas con Matlab.
Pirámide (2007).
[11] Knuth, D. The art of computer programming 1: Fundamental algorithms 2: Seminumerical algorithms 3:
Sorting and searching (1968).
[13] Larson, R. E.; Hostetler, R. P.; Edwards, B. H.; Rapún, L. A. y López, J. L. P. Cálculo y geometría analítica.
McGraw-Hill (1989).
[15] Mathews, J. H. y Fink, K. D. Métodos numéricos con MATLAB. Prentice Hall (2000).
[17] Press, W. H.; Teukolsky, S. A.; Vetterling, W. T. y Flannery, B. P. Numerical recipes in C. Cambridge
university press Cambridge (1996).
[20] Wirth, N. Algoritmos + estructuras de datos = programas. Ediciones del Castillo (1980).
243