Vous êtes sur la page 1sur 59

TEMA 0: Nociones de Algoritmos

0.1 Visión Histórica.

Un algoritmo
algoritmo es el
el conjunto de operaciones
operaciones y procedimientos
procedimientos
que deben seguirse para resolver un problema. La palabra "algoritmo"
deriva del nombre latinizado del gran matemático árabe Mohamed Ibn
Moussa Al Kow Rizmi, el cual escribió sobre entre los años 800 y 825
su obra Quitab Al Jabr Al Mugabala, donde se recogía el sistema de
numeración hindú y el concepto del cero. Fue Fibonacci, el que
tradujo su obra al latín y la inició con las palabras: Algoritmi
dicit.

0.2 Diferencia entre el lenguaje algorítmico y el informático.

El lenguaje algorítmico es aquel por medio del cual se


realiza un análisis previo del problema a resolver y encontrar un
método que permita resolverlo. El conjunto de todas las operaciones
a realizar, y el orden en el que deben efectuarse, se le denomina
algoritmo.

El lenguaje informático es aquel por medio del cual dicho


algoritmo se codifica a un sistema comprensible por el ordenador o
computadora.
computadora . Este tipo de lenguaje es más cercano a la máquina que
al ser humano y podemos distinguir distintos tipos dependiendo de la
proximidad a la maquina. Se denomina lenguaje de alto nivel aquel
que es más cercano
cercano a la comprensión humana y lenguaje
lenguaje de bajo
bajo nivel
a aquellos que son más comprensibles
comprensible s por la máquina. En concreto,
nosotros vamos a estudiar un lenguaje en la frontera de uno de bajo
nivel. Es por ello que el 'C' es tan potente y rápido, pues las
funciones principales representan las funciones más básicas del
ordenador.

0.3 Planteamientos de Problemas.

Lo que pretende un algoritmo es sintetizar de alguna forma


una tarea, cálculo o mecanismo antes de ser transcrito al ordenador.
Los pasos que hay que seguir son los siguientes:

- Análisis previo del problema.


- Primera visión del método de resolución.
- Descomposición
Descomposición en módulos.
- (Programación
(Programació n estructurada)
estructurada)..
- Búsqueda de soluciones parciales.
- Ensamblaje de soluciones finales.

Ejemplo: Calcular las posibles raíces para una ecuación de segundo


grado: ax^2+bx+c=0

+-Algoritmo raíces
|
| Variables reales a,b,c,x,y
|
| Escribir "Introduzca los coeficient
coeficientes
es de mayor a menor grado."
| Leer a,b,c
|
| +-Si sqr(b)>= 4*a*c entonces
| | x=(-b+sqrt(b^2-4*a*c))/
x=(-b+sqrt(b^2-4*a*c))/2a
2a
| +-Sino
| | Escribir "No existen raíces reales."
| +-Finsi
|
+-Final

0.4 Organigrama
Organigramas.
s.

Un organigrama o diagrama de flujos es una representación


representaci ón
semigráfica del algoritmo en cuestión. Esto nos facilita la visión
descriptiva de la ejecución del programa, así como la generación de
la traza del algoritmo. Se denomina traza de un algoritmo a la
ejecución manual ded e un programa obteniendo para cada
ca da paso un
resultado.

Símbolos generales:

* Inicio y fin de un programa.


* Operaciones de I/O , aritméticas y lógico-aritméticas.
* Decisiones lógicas.
* Flujo de la ejecución.

0.5 Traza de un Algoritmo.

La traza de un Algoritmo se puede definir como la ejecución


manual de forma secuencial de las sentencias que lo componen. Así,
la traza del siguiente algoritmo es el valor que van adoptando las
variables a medida que se va ejecutando un programa.

+-Algoritmo Suma
|
| Variable entera a,b
|
| Escribir "Indique el primer sumando"
| Leer a
| Escribir "Indique el segundo sumando"
| Leer b
| c=a+b
| Escribir "El resultado es: ";c
|
+-Final

+----------------------------+
| T R A Z A |
+------------+---------------+
| Comentario | Valores |
+------------+---------------+
| Leemos a: | a <- 4 |
| Leemos b: | b <- 5 |
| Calcula c: | c <- a+b <- 9 |
| Escribe c: | c <- 9 |
+------------+---------------+

La función principal que posee realizar la traza de un


algoritmo es la de comprobar que éste funciona correctamente o para
realizar la etapa de depuración en la que se intenta corregir
errores, simplificar el algoritmo al máximo e incrementar su
eficacia y velocidad.

TEMA 1: Composición de Algoritmos

Los algoritmos están compuestos por diferentes partes, unas


relacionadas
relacionada s íntimamente con las otras, de tal forma que muchas
veces la no existencia
existencia de una provocaría una confusión
confusión en el
el mismo.
Por ello es muy importante el saber las partes principales en las
que se divide los algoritmos y saber cuales son esenciales y cuales
no.

1.1 Cabecera (nombre del Algoritmo).

Al comenzar cualquier algoritmo, este debe ser bautizado, de


tal forma que tan solo leer la cabecera sepamos cual va a ser su
propósito.

Ejemplo:

+-Algoritmo Factorial <- Cabecera


|
| Variable entera a,b,c
| Escribir "Introduzc
"Introduzca
a el número a factorizar.
factorizar."
"
| Leer a
| b = 1
| +-Para c desde 2 hasta a hacer
| ³ b=b*c
| À-FinPara
| Escribir "El factorial es: ",b
+-Final

1.2 Sección de datos (declaración de variables).

Esta parte es esencial para cualquier algoritmo que trabaje


con variables. En esta sección se va a declarar cuales son las
variables con las que vamos a trabajar y cuales son sus tipos.

1.2.1 Tipos.

El tipo de una variables define el contenido de ésta, es


decir, indica cual va a ser el propósito de la variable.

Los tipos de datos estándar son:

- Enteras (Su contenido será un número entero)


- Reales (Su contenido será un número real)
- Carácter (Su contenido será un carácter alfanumérico
alfanumérico))
- Cadena (Su contenido será un un conjunto de caracteres)
- Lógicas (Su valor indica un hecho cierto o falso)

Existen otros tipos de variables tales como byte, word,


dbyte, dword, etc. Que son variables más encaminadas a la
programación profesional.

Las variables se caracterizan


caracteriza n pues poseen una jerarquía que
viene definida por el número de bytes que se asignan para cada una.
Así un carácter posee un longitud de un byte, (donde se almacena un
número al que se le ha asociado mediante la norma ASCII) sin embargo
un entero posee dos byte. Sería lógico pensar que una variable
entera contuviera a un carácter y de hecho esto puede ser así, sin
embargo el mezclar tipos de variables es impropio de una
programación
programació n ordenada y elegante. Es decir, no se debe mezclar
tipos de variables a no ser que se produzca a través de una función
de conversión de tipos (convertir un entero a una cadena y
viceversa).
En el programa anterior se observa la declaración de
variables después de la cabecera, que es el orden que debe seguirse
en la elaboración de un algoritmo y en un programa informático.

1.2.2 Variables y Constantes.

La principal diferencia entre variables y constantes es que


las primeras pueden variar a lo largo de la ejecución del programa,
mientras que las segundas permanecen constantes siempre. Las
constantes se declaran después de la cabecera y antes de las
variables.

Ejemplo:

+-Algoritmo Circunferen
Circunferencia
cia
|
| Constante real PI=3.1416
| Variable real r,c
|
| Escribir "Introduzca el radio de la circunferen
circunferencia"
cia"
| Leer r
| c=2*Pi*r
| Escribir "Su longitud es: ",c
|
+-Final

Se define Vector como una variable cuya estructura es una


sucesión de elementos del mismo tipo. Así una variable de cadena es
un vector de caracteres, ya que esta formado por una sucesión de
variables del tipo carácter. Así podemos crear vectores de
diferentes tipos.

Ejemplo: Producto escalar de dos vectores en una base ortonormal.

+-Algoritmo Producto_Es
Producto_Escalar
calar
|
| Vector entero a[1..3], b[1..3]
| Variable entera c
|
| Escribir "Introduzc
"Introduzca
a el vector A (x,y,z)"
| Leer a[1],a[2],
a[1],a[2],a[3]
a[3]
| Escribir "Introduzc
"Introduzca
a el vector B (x,y,z)"
| Leer b[1],b[2],
b[1],b[2],b[3]
b[3]
|
| c=a[1]*b[1]+a[2]*b[2]+a[3]*b[3]
|
| Escribir "El producto escalar es: ",c
|
+-Final

De igual forma tenemos que una matriz es un vector de


vectores que se define como:

¦ matriz entera Rotacional [1..3,1..3]

Hay que decir que el concepto de [Columnas,Filas]


[Columnas,F ilas] o [Filas,
Columnas] es más bien arbitrario, ya que podemos adoptar el formato
que queramos, siempre y cuando lo mantengamos a lo largo del
programa.
TEMA 2: Sección del Código

Es esta sección, la que se puede considerar como el corazón


del algoritmo. En ella van los procedimientos,
procedimient os, las funciones y el
cuerpo del programa, dentro de los cuales van las sentencias que
indican los pasos a realizar por el programa.

2.1 Bloque del Programa.

El bloque del programa es como el centro neurálgico del


programa, desde él, se controla
controla las entradas
entradas a los
los procedimientos
procedimientos y
funciones principales (aunque estos pueden llamar a otros
procedimientos
procedimien tos y funciones secundarios). En el programa anterior se
representa como la siguiente parte:

+-Algoritmo Circunferencia
|
| Constante real PI=3.1416
| Variable real r,c
|
+---->Escribir
+---->Escribir "Introduzca el radio de la circunferen
circunferencia"
cia"
Bloque | | Leer r
del | | c=2*Pi*r
Programa+---->Escribir
Programa+---->Escribir "Su longitud es: ",c
|
+-Final

2.2 Procedimientos y Funciones.

Tanto los procedimientos


procedimient os como las funciones son los módulos
en los que se puede descomponer un algoritmo. Cada modulo se encarga
de realizar una operación independiente
independien te de los restantes desde el
punto de vista funcional pero este puede estar relacionado con otros
procedimientos y funciones para el intercambio de valores de
variables. Hay que decir, que cualquier algoritmo se puede
transformar en un procedimiento
procedimien to para ser utilizado dentro de otro
algoritmo mayor. Así, uno de los programas anteriores quedaría de la
siguiente forma:

+-Procedimiento Factor(a,b) <- Parámetros Formales.


|
| Parámetro real a,b
| Variable entera c
|
| b = 1
| +-Para c desde 2 hasta a hacer
| | b=b*c
| +-FinPara
|
+-FinFactor

2.2.1 Parámetros Formales y Actuales.

Como hemos visto, entre los procedimientos


procedimien tos (funciones
también) y su entorno se producen una relación en base a un
intercambio de valores de las variables. Estas variables reciben
nombres diferentes según este en el código Padre o en el código
Hijo. Vamos a definir como código Padre, aquel desde el cual se
llama a una subrutina y, el código Hijo, la subrutina que estamos
llamando.
Parámetros Actuales son los que utiliza el programa Padre
para relacionarse con una subrutina en concreto, y parámetro
Formales son los que posee el programa Hijo y que lo relaciona con
el Padre.

+-Algoritmo Factorial
|
| Variable real num,valor
|
| Escribir "Introduzc
"Introduzca
a el número a factorizar:
factorizar:"
"
| Leer num
|
| Factor(num,valor)
Factor(num, valor) <- Llama al Procedimie
Procedimiento
nto Factor
| | |
| +---+---> Parámetro Actuales.
|
| Escribir "El factorial es: ",valor
|
+-Final

Obsérvese que los parámetros actuales y formales no tienen


por que llamarse de igual forma, sin embargo es condición necesaria
que sean del mismo tipo y que estén en el mismo orden.

La transmisión de un parámetro como valor y no como


variable, hace que el programa Padre no reciba las posibles
modificaciones
modificaciones que pueda sufrir dicho parámetro dentro del código
Hijo.

Ejemplo:

+-Algoritmo Factorial Constante


|
| Variable real num,valor
|
| Escribir "Introduzc
"Introduzca
a el número a factorizar:
factorizar:"
"
| Leer num
|
| valor=0
| Factor(num,3)
Factor(num,3 ) <- Llama al Procedimient
Procedimiento
o Factor
|
| Escribir "El factorial es: ",valor
|
+-Final

Se puede observar claramente que siempre que introduzcamos


cualquier número vamos a obtener que el factorial es 0, pues al
introducir la variable de forma numérica no se actualiza en el
procedimiento.

2.2.2 Variables Globales y Locales.

De igual forma que en el apartado anterior diferenciábamos


diferenciábam os
entre parámetros formales y actuales, ahora vamos a realizar una
distinción entre variables
v ariables globales y locales.
loca les. Podemos
Po demos definir
variable global como aquella que puede ser utilizada (leída,
modificada, etc.) a lo largo de todo el algoritmo principal y
también por
por cualquiera
cualquiera de los subalgoritmos
subalgoritmos (entiéndase
(entiéndase funciones y
procedimientos) que componen el algoritmo en sí. De igual forma, una
variable local, es aquella que sólo puede ser referenciada dentro
del subalgoritmo en el cual ha sido declarada. Para simplificar,
podemos decir que las variables globales pueden ser referenciadas
referenciadas
desde cualquier parte del algoritmo mientras que las locales
únicamente serán referenciadas dentro del subalgoritmo al que
pertenece:

+--------------------+------------------------+
| Variables Locales | Subalgoritm
Subalgoritmo
o propio |
| Variables Globales | Cualquier subalgoritmo |
+--------------------+------------------------+

TEMA 3: Sentencias

Las sentencias o instrucciones se pueden dividir


principalmente
principalmente en tres grandes grupos:

- Sentencias Simples.
- Sentencias Compuestas.
- Sentencias de control del flujo del algoritmo.

Las Sentencias Simples son del tipo de:

- Asignación de Variables y Constantes.


- Llamadas a Procedimient
Procedimientos
os y Funciones, dentro de estas últimas
englobamos todas las funciones y procedimiento
procedimientos
s que conforman la
librería general de sentencias que veremos posteriormente.

Las Sentencias Compuestas:

- Son aquellas que están limitas dentro de Procedimie ntos


Procedimientos o
Funciones.

Las Sentencias de Control de Flujo:

- Sentencias Reiterativ
Reiterativas:
as: Mientras, Repetir, Para.
- Sentencias Condicionales: Si, Case... of

3.1 Aritméticas y varias funciones.

Dentro de la asignaciones de variables juega un gran papel


los operadores y funciones matemáticas, tales como:

+-------------+-----------------------------------------------+
| + | Suma |
| - | Resta |
| * | Producto |
| / | División (devuelve un valor real) |
| div | División (devuelve un valor entero) |
| mod | Cálculo del módulo aritmético. |
| log | Logaritmo en base 10 |
| ln | Logaritmo neperiano |
| exp | Exponencial de un número |
| pow | Potencia de un número |
| random | Obtención de un número aleatorio |
| abs | Obtenemos el valor absoluto de un número |
| sqr | Obtención del cuadrado de un número |
| sqrt | Obtención de la raíz cuadrada |
| sin,cos,tan | Funciones trigonométric
trigonométricas
as |
| chr/toascii | Obtenemos un carácter a partir de un número |
| ord | Obtenemos el número correspondien
correspondiente
te al código |
| | ASCII |
+-------------+-----------------------------------------------+

Lógicamente, existen más funciones aritméticas que veremos


más adelante.

3.2 Sentencias de Control.

Como ya dijimos en la introducción del tema 3, las


sentencias de control son aquellas que interrumpen la ejecución
secuencial de las instrucciones
instruccione s de un algoritmo, permitiendo la
generación de reiteraciones.

3.2.1 Condicionales (Si y Case).

La función SI, viene acompañada por una serie de elementos


que son los operadores relacionales y operadores booleanos.

Los operadores relacionales son los siguientes:

+----+-----------------+
| = | Igual a |
| < | menor que |
| > | mayor que |
| <= | menor o igual a |
| >= | mayor o igual a |
| <> | distinto de |
+----+-----------------+

Estos operadores nos van a servir generalmente para comparar


y comprobar los valores que toman las variables a lo largo del
algoritmo, y dependiendo de los resultados, realizar una cosa u
otra.

Operadores booleanos:

Los operadores booleanos nos permiten generar condiciones


compuestas. (AND, OR, XOR, NOT)

Ejemplo: ¨Es la variable b mayor que 12 y la variable d igual a


cierto?

Operador AND: (debe cumplirse las dos condiciones)

+--------------------------+
| true AND true -> true |
| true AND false -> false |
| false AND true -> false |
| false AND false -> false |
+--------------------------+

Operador OR: (debe cumplirse una de las dos condiciones)

+-------------------------+
| true OR true -> true |
| true OR false -> true |
| false OR true -> true |
| false OR false -> false |
+-------------------------+

Operador XOR: (se cumple cuando las dos condiciones son distintas)
+--------------------------+
| true XOR true -> false |
| true XOR false -> true |
| false XOR true -> true |
| false XOR false -> false |
+--------------------------+

Operador NOT: (niega el resultado de una condicion)

+--------------------+
| NOT true -> false |
| NOT false -> true |
+--------------------+

- La sentencias SI puede tener las siguientes estructuras:

+-Si (condición) entonces


| ...
+-Sino
| ...
+-Finsi

También puede aparecer en estructuras más complejas:

+-Si (condición1) entonces


| +-Si (condición2) entonces
| | ...
| +-Finsi
+-Sino
| ...
| +-Si (condición3) entonces
| | ...
| +-Sino
| | ...
| +-Finsi
+-Finsi

A este tipo de estructuras se le denomina "anidamiento o


encadenamiento
encadenamiento de sentencias SI".

- En cuanto a la sentencia EN CASO hay que decir, que se trata de


una simplificación de sentencias SI anidadas. De esta forma, sea una
variable A tenemos la siguiente estructura de Si encadenados.

+-Si (condición1) entonces


| acción1
+-Sino Si (condición2) entonces
| acción2
+-Sino Si (condición3) entonces
| acción3
+-Sino
| acción4
+-Finsi

Con una estructura del tipo EN CASO, tendríamos el problema


problema
resuelto de la siguiente forma:

+-En Caso de Variable


| condición1: Acción1
| condición2: Acción2
| condición3: Acción3
+-En Otro Caso
| Acción4
+-Fincaso

3.2.2 Bucles Mientras, Repetir y Para.

Las iteraciones son otro tipo de sentencias de control. Las


que veremos son las siguientes: Mientras, Repetir y Para. Siendo
este último uno de los más usados. La utilización de un bucle en un
programa permite la posibilidad de realizar iteraciones de secciones
de código, evitando así andar con condiciones,
condiciones , etiquetas y la
sentencia GOTO. Esta última sentencia GOTO, hay que evitarla en por
todos los medios. Cualquier programa puede ser realizado sin tener
que utilizar dicha sentencia; ya que su uso crea confusión a la hora
de seguir la ejecución secuencial de las sentencias de un algoritmo.

3.2.2.1 Bucle Mientras.

El bucle Mientras se caracteriza por ser utilizado cuando no


conocemos el número de iteraciones con antelación. Es por ello que
nos ayudamos de una comprobación
comprobació n o condición para la entrada/salida
entrada/sali da
del mismo antes
antes de realizar
realizar la ejecución de la sección
sección del código
código a
repetir; esto último nos posibilita el caso de no efectuar ninguna
iteración (iteración=0).
(iteración=0).

+-Algoritmo Multiplicar (mult1,mult2,resul)


| Parámetros reales mult1,mult2
mult1,mult2,resul
,resul
|
| resul=0
| +-Mientras mult2>0 hacer
| | resul=resul+mult1
resul=resul+mult1
| | mult2=mult2-1
mult2=mult2-1
| +-Finmientra
+-Finmientras
s
+-Final

3.2.2.2 Bucle Repetir.

El bucle Repetir se caracteriza porque al igual que el


anterior no sabemos el número de iteraciones que debemos realizar,
es por ello que se apoya en condiciones para salir del mismo. Al
contrario que el anterior, la comprobación va al final del bucle, de
esta forma, como mínimo siempre se produce una iteración. Veamos el
algoritmo anterior utilizando el bucle repetir.

+-Algoritmo Multiplicar (mult1,mult2,resul)


| Parámetros reales mult1,mult2
mult1,mult2,resul
,resul
|
| resul=0
| +-Repetir
| | resul=resul+mult1
resul=resul+mult1
| | mult2=mult2-1
mult2=mult2-1
| +-Hasta que mult2<=0 (o también hasta que not (mult2 >0) )
+-Final

Una forma muy sencilla para pasar un bucle Mientras a


Repetir, es hallando la condición opuesta, o bien poniendo un not en
la comprobación, negando así la condición del mientras. También se
puede hallar teniendo en cuenta el significado
significado de los operadores
relacionales y booleanos.
+----------+----+
| NOT (<) | >= |
| NOT (<=) | > |
| NOT (>) | <= | ¦ * Negación de lo operadores ralacionales.
| NOT (>=) | < |
| NOT (=) | <> |
| NOT (<>) | = |
+----------+----+

3.2.2.3 Bucle Para

Lo que caracteriza al bucle Para es que ya sabemos con


antelación el número de iteraciones a realizar. Es por ello que no
nos hace falta una comprobación
comprobació n de salida y/o entrada. También
existe la posibilidad de realizar 0 iteraciones, cuando la variable
secundaria es menor que la primaria. Otra característica,
característi ca, es la
posibilidad de realizar incrementos de n en n en el contador del
bucle. Pasemos a ver el algoritmo anterior mediante un bucle Para.

+-Algoritmo Multiplicar (mult1,mult2,resul)


| Parámetros reales mult1,mult2
mult1,mult2,resul
,resul
| Variable Local loop
| resul=0
| +-Para loop=1 hasta mult2 de incremento 1 hacer
| | resul=resul+mult1
resul=resul+mult1
| | mult2=mult2-1
mult2=mult2-1
| +-Finpara
+-Final

En este caso, la variable primaria es "loop" y la secundaria


es "mult2". Si esta última toma un valor inferior a la primaria,
entonces el bucle no se realiza. Obsérvese que hemos puesto el
incremento= 1, este es el valor que posee el bucle Para por defecto,
es por ello que cuando el incremento es de 1 en 1, no se debe
especificar.

TEMA 4: Manejo de Archivos

Dentro de este apartado vamos a aprender que son y como


utilizar los archivos. Veremos cual es su finalidad, que tipos son
los más comunes y cuales son sus características
características principales desde
el punto de vista de la organización
organización de los datos que contiene.

Para comenzar podemos decir que un ordenador que no tiene la


posibilidad de almacenar
almacenar sus programas y datos en un dispositivo
dispositivo de
almacenamiento,
almacenamie nto, (ya sean discos, cintas, etc.) no es mas que una
calculadora. En la actualidad, cualquier ordenador posee
dispositivos
dispositivo s de almacenamiento,
almacenamie nto, ya sean internos (discos duros) o
externos (disquetes, cartuchos, cintas, etc.). La finalidad
fin alidad es
obvia, la de poder guardar los datos para su posterior recuperación
y tratamientos de los mismos en otras sesiones.

Un común error entre los principiantes es el de confundir la


memoria de tipo RAM con la capacidad de almacenamiento del disco que
acompaña al ordenador. La RAM (Memoria de Acceso Aleatorio) es
denominada memoria volátil, ya que una vez que se apaga el ordenador
la información que esta contenía se pierde. La RAM se mide
generalmente
generalment e en MegaBytes, aunque con el paso del tiempo la unidad
puede cambiar (1 Mb son 1024 Ks, a su vez 1 K es 1024 bytes, y
finalmente 1 Byte son 8 bits, siendo esta última la cantidad mínima
de información que puede procesar un computador). La capacidad de
los dispositivos de almacenamiento
almacenamie nto (entiéndase disquetes, discos
duros, cintas, CD-ROM, etc.) se mide en las mismas unidades, es por
ello que la gente suele confundir la memoria RAM de un ordenador con
la capacidad de almacenamiento que suele tener en un disco duro.

La memoria RAM es uno de los aspectos que limita la potencia


de un ordenador, cuanta más RAM tengamos, mayores programas y datos
podremos almacenar en ella y menos accesos a los dispositivos de
almacenamiento
almacenamiento tendremos que realizar.

Los archivos o ficheros se almacenan en los dispositivos de


almacenamiento,
almacenamie nto, para como dijimos anteriormente
anteriormen te puedan ser
recuperados sus datos en sesiones futuras.

De esta forma, podemos definir un fichero como un objeto


concebido para el almacenamiento
almacenamie nto permanente
perm anente de la información.
Información que puede ser organizada de diferente forma dependiendo
del trato que le vayamos a dar.

4.1 Nociones de dispositivos de almacenamiento.

En el apartado anterior hemos aprendido que la información


puede ser volcada en dispositivos
dispositivos de almacenamiento
almacenamiento permanente, a
los cuales nos hemos referido como disquetes, discos duros, cintas,
CD-ROM, etc.

Estos medios de almacenamiento


almacenamie nto se pueden diferenciar en el
modo en el que la información puede ser accesible.

Así definimos dispositivos de almacenamiento


alm acenamiento o soportes
secuenciales
secuenciale s a aquellos donde la información es accesible
secuencialmente,
secuencialmente, es decir, para leer o escribir un dato determinado,
antes tenemos que pasar por todos los datos que le preceden. De esta
forma, en una cinta magnética los datos se graban y leen uno detrás
de otro. Y no podemos ir directamente a uno en concreto sin pasar
antes por los demás.

De igual forma se define soportes direccionables


direccionabl es a aquellos
donde la superficie de almacenamiento
almacenamien to es independiente
independient e e
individualmente
individualm ente direccionable,
direccionabl e, es decir, podemos acceder a un dato
en concreto sin tener que pasar antes por los demás. Este es el caso
de los discos. Por poner un ejemplo, en un disco de vinilo, podemos
elegir la canción que queremos tan sólo con poner el cabezal en el
comienzo de la misma. Sin embargo, en una cinta de casete, para oír
una canción
ca nción determinada
de terminada antes debemos pasar todas t odas las
la s que le
preceden.

En los dispositivos de almacenamientos


almacenamient os hay que diferenciar
las direcciones en las que se almacenan los datos. De este modo
tenemos dos tipos de direcciones:
direcciones : direcciones absolutas o físicas y
direcciones relativas o lógicas.

Las direcciones absolutas o físicas son las direcciones con


las que juega el ordenador de forma física en el dispositivo, es
decir, cuando el ordenador se refiere a un dato en concreto lo esta
haciendo por el conocimiento de ciertos datos que identifican la
posición física del dato en el soporte de almacenamiento.
almacenamien to. Así, el
ordenador juega con parámetros tales como: unidad, cabeza, cara,
pista o cilindro y sector.
Por el contrario, las direcciones relativas o lógicas son
con las que vamos a jugar
jugar nosotros, una vez que abrimos el fichero
fichero
para realizar operaciones de Input/output (I/O) nos referimos al
dato con un índice numérico, es decir, el registro número x.

4.2 Ficheros de Acceso Secuencial.

Un Fichero de Acceso Secuencial es aquel donde los registros


están ubicados consecutivamente
consecutiva mente sobre un dispositivo de
almacenamiento. De tal forma que para acceder a un registro
determinado 'd' debemos pasar obligatoriamente
obligatoriam ente por todos los
registros que le preceden.

Suponiendo una cinta


c inta magnética, para leer registro 'd'
debemos pasar antes por el 'a', 'b', 'c'.

----------------------
------------- --------- Para leer el registro 'd', la cabeza
+- a b c d ... lectora, deberá pasar antes por los
| -------------
----------------------
--------- que le preceden.
+> cabeza lectora.

Un Fichero de Acceso Secuencial puede ser almacenado tanto


en un dispositivo de almacenamiento direccional como secuencial, sin
embargo los Ficheros de Acceso Directo e Indexado únicamente puede
ubicarse en dispositivos de almacenamiento direccional.

4.2.1 Ejemplo de I/O en Ficheros Secuenciales.


Secuenciales.

Pasemos a ver un ejemplo de como utilizar ficheros


secuenciales,
secuenciale s, pero antes veamos las sentencias básicas que
utilizaremos.

- Abrir secuencial (variable)

Esta sentencia abre un fichero para el acceso secuencial.


Donde la variable contiene el nombre del fichero a abrir.

- Cerrar (variable)

Esta sentencia cierra un fichero donde la variable contiene


el nombre del fichero a cerrar.

- Leer/Escribi
Leer/Escribir/Reescribi
r/Reescribir
r (variable1,v
(variable1,variable2)
ariable2)

Esta sentencia permite la lectura o re/escritura


re/escritur a de los
datos que contiene
cont iene la variable2 en un fichero de nombre,
nom bre, el
determinado en la variable1.

- Iniciar operaciones de lectura y/o escritura en (variable)

Esta sentencia debe incluirse después de abrir un fichero y


determina prácticamente
prácticament e el tipo de acceso que vamos a realizar, es
decir, si abrimos el fichero para leer y/o escribir.

- NO fin (variable)

Esta función devuelve el valor lógico 'true' si no se ha


encontrado el final del fichero; y devuelve el valor lógico 'false'
si se ha encontrado el final del fichero.
- Error (variable)

Esta función devuelve el código del error producido al


realizar cualquier operación anterior. Por defecto,
def ecto, la
l a función
devuelve un cero cuando no se ha producido ningún error.

Ejemplo: En este ejemplo se intenta dar aun visión general del uso
de ficheros secuenciales.
secuenciales.

+-Algoritmo Copiar_fich
Copiar_fichero
ero
|
| Fichero de enteros Origen,Desti
Origen,Destino
no
| Variable entera x
|
| Escribir ("Indique el nombre del fichero Origen:");
| Leer Origen
| Escribir ("Indique el nombre del fichero Destino:")
| Leer Destino
|
| Abrir secuencial (Origen)
| Iniciar lectura en (Origen)
| Abrir secuencial (Destino)
| Iniciar escritura en (Destino)
|
| +-Mientras (NO fin(Origen)) hacer
| | Leer (Origen,x)
| | Escribir (Destino,x)
| +-Finmientra
+-Finmientras
s
|
| Escribir ("Fichero Copiado:")
|
| Cerrar (Origen)
| Cerrar (Destino)
|
+-Final

Nota: Obsérvese que la variable utilizada en las operaciones


de lectura y/o escritura, deben ser del mismo tipo que la
declaración del fichero.

4.3 Registros Estructurad


Estructurados.
os.

Hasta ahora hemos visto como los registros eran de un tipo


único, es decir, eran todos carácter, enteros, reales, etc. Sin
embargo, hay situaciones en las que debemos realizar agrupaciones de
tipos para formar un registro estructurado. Así, un registro
estructurado esta formado por un conjunto de variables de diferentes
tipos que denominaremos
denominaremos campos. De este modo podemos decir que un
registro estructurado es un conjunto de campos.

Ejemplo:

+-Registro datos-alumno
| Cadena nombre
| Cadena apellido1
| Cadena apellido2
| Cadena NIF
| Cadena curso
| Cadena telefono
| Cadena fecha_nac
| ...
+-Finregistro

De esta forma, podemos leer y escribir en un fichero


registros estructurados que nos permitirán un almacenamiento
almacenamien to más
lógico y ordenado de los datos.

Al generar un registro es como si estuviésemos definiendo un


nuevo tipo de variable independiente
independient e de las ya existente (enteras,
reales, cadena, etc.).

Este tipo de registros o record se utiliza para agrupar la


información que se desee volcar a un fichero determinado.

4.4 Ficheros de Acceso Directo.

Un Fichero de Acceso Directo (es también denominado de


acceso aleatorio) es aquel que se encuentra almacenado en un
dispositivo direccionable;
direccionable; y donde sus registros poseen un campo que
denominaremos
denominarem os campo clave y que identifica inequívocamente
inequívocame nte a cada
registro. En los ficheros de acceso directo el campo clave es el
número del registro en el fichero. Así se establece una
correspondencia
corresponde ncia directa entre los valores del campo clave y las
direcciones lógicas en el soporte. Los registros se almacenan según
el orden de entrada y no quedan ordenados.

De esta forma en un Fichero de Acceso Directo nos referimos


a un registro por medio de su posición en este. Así, podremos
obtener el reg número 4 sin pasar antes por los demás.

+-----+------------+
| Reg | D A T O |
+-----+------------| Fíjese que los datos están
| 1 | LUIS | almacenados en el orden en el
| 2 | CARLOS | que han sido introducidos por el
| 3 | TERESA | usuario.
| 4 | JOAQUIN |
| 5 | INMA | Accedemos a los datos por medio
| 6 | JOSE | del valor de la posición del
+-----+------------+ registro.

La tabla anterior se denomina tabla de acceso. Esta tabla


relaciona de forma única el número del registro con el registro
correspondiente, así el reg número 2 corresponde al dato Carlos.

4.4.1 Ejemplo de I/O en Ficheros de Acceso Directo.

Las sentencias que manejan los ficheros de acceso directo


son las mismas, sólo que poseen el prefijo directo.

Ejemplo:

+-Algoritmo Contador_de
Contador_de_registros.
_registros.
|
| Fichero de enteros F
| Variable entera x,contador
|
| Abrir directo (F)
| Iniciar lectura (F)
| contador = 0
| +-Mientras (NO fin(F)) hacer
| | Leer directo(F,
directo(F,x)
x)
| | contador=contador+1
contador=contador+1
| +-Finmientra
+-Finmientras
s
| Cerrar(F)
|
| Escribir ("El fichero:";F;
fichero:";F;"posee:";co
"posee:";contador;"reg.
ntador;"reg.")
")
|
+-Final

4.5 Ficheros de Acceso Indexado.

Un Fichero de Acceso Indexado es aquel que se encuentra


almacenado en un dispositivo direccionable;
direccionable ; y donde sus registros
poseen un campo que denominaremos
denominaremo s campo clave principal y que
identifica inequívocamente a cada registro. La clave principal debe
ser aquel campo del registro estructurado
estructurado que tenga siempre un valor
diferente a los ya introducidos,
introducido s, es decir, dentro del fichero
Indexado no puede haber dos registros con los campos clave principal
iguales.

Además del campo clave principal pueden existir otros campos


claves secundarios
secundarios que realizan la misma tarea que el campo clave,
sin embargo, sus valores pueden repetirse. En los Ficheros de Acceso
Indexado el campo clave puede ser cualquiera de los campos de un
registro estructurado. Así se establece una correspondencia
corresponden cia directa
entre los valores del campo clave y el propio registro al que
pertenece. Los registros se almacenan ordenados alfabéticamente
alfabéticame nte por
el campo clave, esto nos facilita la búsqueda y listados ordenados
por los distintas claves.

Para cada campo clave, el fichero genera una tabla, donde


dicha clave aparece ordenada alfabéticamente
alfabéticamente y se relaciona con la
dirección de los datos.

De esta forma en un Fichero de Acceso Indexado nos referimos


a un registro por medio de alguna de las claves que posea el
fichero, tanto la principal como la secundaria. Es decir, leer el
registro cuya clave principal sea: 46.399.554,
46.399.554, en este caso leería
el registro correspondiente
correspondiente a INMA. También podríamos haber dicho,
leer los registro cuya clave secundaria sea la Edad=16 y primero nos
leería el registro correspondiente
correspondiente a los datos de Luis y en la
siguiente petición de lectura los datos de Teresa. La diferencia
entre clave principal y secundaria, está en que la clave principal
es única
úni ca (relacionando
(rela cionando así inequívocamente
ine quívocamente al registro
reg istro al que
pertenece) mientras que las claves principales puede ser iguales.

+------------+-------------------------------------+
| Clave | Clave Clave Clave |
| Principal | Secundaria Secundaria Secundaria |
+--------------------------------------------------+
+---------+------------+------------+-------------+--------+
| (Direc) | (D.N.I.) | (Nombre) | (Provincia) | (Edad) |
+---------|------------|------------|-------------|--------+
| 1 | 55.366.546 | LUIS | Las Palmas | 16 |
| 2 | 42.386.225 | CARLOS | Salamanca | 17 |
| 3 | 32.387.203 | TERESA | Oviedo | 16 |
| 4 | 46.399.554 | INMA | Palencia | 20 |
| 5 | 60.643.434 | JOAQUIN | Salamanca | 17 |
| 6 | 22.543.986 | JOSE | Las Palmas | 23 |
+---------+------------+------------+-------------+--------+
Como podemos observar, esto sería un ejemplo de un fichero
indexado.

Para cada campo clave, el fichero genera una tabla, donde


dicha clave aparece ordenada alfabéticamente
alfabéticamente y se relaciona con la
dirección de los datos. Así las tablas para la clave principal (DNI)
y la clave secundaria (Nombre) serían:

+------------+---------+ +------------+---------+
| (D.N.I.) | (Direc) | | (Nombre) | (Direc) |
+------------+---------| +------------+---------+
| 22.543.986 | 6 | | CARLOS | 2 |
| 32.387.203 | 3 | | INMA | 4 |
| 42.386.225 | 2 | | JOAQUIN | 5 |
| 46.399.554 | 4 | | JOSE | 6 |
| 55.366.546 | 1 | | LUIS | 1 |
| 60.643.434 | 6 | | TERESA | 3 |
+------------+---------+ +------------+---------+
+--------------------------+ +---------------------------+
| Tabla de Ac. Clave Princ.|
Princ.| | Tabla de Ac. Clave
Clave Secund.|
Secund.|
+--------------------------+ +---------------------------+

Obsérvese como ambas tablas aparecen ordenadas


alfabéticamente
alfabéticam ente (o de menor a mayor en el caso del DNI). Como ya
dijimos, esto nos da grandes facilidades a la hora de realizar
búsquedas y/o listados ordenados.

4.5.1 Ejemplo de I/O en Ficheros de Acceso Indexado.

Pasemos a ver un ejemplo de como utilizar ficheros


indexados, pero antes
an tes veamos
ve amos las
l as sentencias
sent encias básicas que
utilizaremos.

- Abrir indexado (variable,KEY=...)


(variable,KEY=...)

Esta sentencia abre un fichero para el acceso indexado.


Donde la variable contiene el nombre del fichero a abrir y en 'KEY='
ponemos los campos claves separados por comas, comenzando por el
campo clave principal.

- Cerrar (variable)

Esta sentencia cierra un fichero donde la variable contiene


el nombre del fichero a cerrar.

- Leer/Escribi
Leer/Escribir/Reescribi
r/Reescribir
r (variable1,K
(variable1,KEY=,variabl
EY=,variable2)
e2)

Esta sentencia permite la lectura o re/escritura


re/escritur a de los
datos por medio de un campo clave (principal o secundaria) que
debemos poner después de 'KEY='. El contenido será almacenado o
mandado por la variable2 en un fichero de nombre, el determinado en
la variable1.

- Iniciar operaciones de lectura y/o escritura en (variable)

Esta sentencia debe incluirse después de abrir un fichero y


determina prácticamente
prácticament e el tipo de acceso que vamos a realizar, es
decir, si abrimos el fichero para leer y/o escribir.

- NO fin (variable)
Esta función devuelve el valor lógico 'true' si no se ha
encontrado el final del fichero; y devuelve el valor lógico 'false'
si se ha encontrado el final del fichero.

- Error (variable)

Esta función devuelve el código del error producido al


realizar cualquier operación anterior. Por defecto,
def ecto, la
l a función
devuelve un cero cuando no se ha producido ningún error.

Las sentencias que manejan los ficheros de acceso Indexado


son las mismas que hemos utilizado en los ficheros secuenciales,
sólo que poseen el prefijo Indexado y en las operaciones de lectura
y/o escrituras hay que indicar la clave ('KEY=').

Ejemplo:

+-Algoritmo Buscar_Pers
Buscar_Persona.
ona.
|
| +-Registro estructurado datos_personales
| | Variable cadena dni
| | Variable cadena nombre
| | Variable cadena tlf
| | Variable cadena provincia
| +-Finregistr
+-Finregistroo
|
| Fichero de datos_person
datos_personales
ales F
| Variable cadena documento
| Variable de datos_perso
datos_personales
nales dato
|
| Escribir "Indique el DNI de la persona a buscar"
| Leer documento
|
| Abrir indexado (F,KEY=dni,n
(F,KEY=dni,nombre)
ombre)
| Iniciar lectura (F)
|
| Leer (F,KEY=doc
(F,KEY=documento,dato)
umento,dato)
| +-Si error(F)<>
error(F)<>00 entonces
| | Escribir "Ese registro no existe."
| +-Sino
| | Escribir " DNI: ";dato.dni
| | Escribir " Nombre: ";dato.nombr
";dato.nombre
e
| | Escribir " Tlf: ";dato.tlf
| | Escribir "Provincia: ";dato.provi
";dato.provincia
ncia
| +-Finsi
|
| Cerrar (F)
+-Final

4.6 Función de Hashing.

Muchas veces
veces surge el caso del que el fichero
fichero es tan grande
grande
que la tabla no puede mantener una ordenación eficaz debido a que
cuando introducimos
introducimos un nuevo dato debe hacerse un espacio en la
misma para albergar a éste. Es por ello que recurrimos al "Hashing".

El "Hashing" consiste simplemente en relacionar la clave


principal con la dirección por medio de una fórmula matemática. Así,
antes de introducir datos se crean unos espacios para albergar las
futuras modificaciones
modificaciones y adiciones de datos. Este método crea una
serie de conjuntos llamados "Buckets". A cada Bucket le corresponde
un número que será el que devuelva la fórmula matemática. A su vez
los Buckets poseen un número que determina la cantidad máxima de
claves que pueden almacenarse en él.

De esta manera cuando vamos a buscar el dato "Manolo" el


Hashing nos determina la posición del conjunto (Buckets). En ese
conjunto habrá otra serie de datos a los cuales les corresponde el
mismo valor de la función Hashing. La búsqueda ahora se hará de
forma secuencial a lo largo del Bucket.

Veamos un ejemplo:

Bucket Clave Prin. Claves Secundarias


+-----+------------+----------------------+
| | Manolo | . . . |
| +------------+----------------------+
| 35 | Manuel | . . . | Al número 104
+-----+------------+-----
+-----+------ ------+------------------
-----------------+
----+ se le denomina
+--| 104 | Manuela | . . . | puntero de
| +-----+------
+-----+------------+-----
------+------------------
-----------------+
----+ desbordamiento
desbordamien to
| | | Natalia | . . . |
| | +------------+-----------
+------------ +----------------------+
-----------+
| | 36 | Naranjo | . . . |
| | +------------+-----------
+------------ +----------------------+
-----------+
| | | Noelia | . . . |
| +-----+------------+----------------------+
| .
| .
| .
| +-----+------------+----------------------+
| | | Mark | . . . |
| | +------------+-----------
+------------ +----------------------+
-----------+
+->| 104 | Miguel | . . . |
| +------------+----------------------+
| | María | . . . |
+-----+------------+----------------------+

4.6.1 Gestión de las colisiones.

En este método parecen una serie de conflictos cuando las


claves son muy parecidas, como podemos observar para claves casi
idénticas, el Hashing nos devuelve el mismo Bucket. Esto implica que
el Bucket puede llenarse de datos; cuando esto ocurre la solución
está en un puntero
puntero que existe en cada Bucket
Bucket que determina
determina el salto
salto
a otro Bucket. Así, cuando se llena el Bucket número 35, existe un
salto de éste
éste al número 104 (otro Bucket)
Bucket) que posee datos del mismo
tipo, que también puede rebosarse y apuntar a otro Bucket secundario
y así sucesivamente.
sucesivamente.

Ahora es cuando surgen los problemas. Cuando un dato se


borra de un Bucket
Bucket hay que
que reorganizar la información
información para no dejar
espacios en blanco dentro
dentro de la tabla. Esto se realiza por medio de
de
un empaquetamiento
empaquetamie nto Packed. Sin embargo cuando se va a realizar
muchas modificaciones
modificacione s y/o borrados y el fichero es muy grande, es
aconsejable hacer una actualización de los datos del fichero.

4.7 Esquema Básico de los tres tipos de Organización.

* Fichero de Acceso Secuencial:


- Almacenamiento en dispositivo secuencial o direccionable.
- No existe campos claves que relacione a algún registro.
- Los datos están almacenados en el orden en el que han sido
introducidos.
- El acceso a los registros es únicamente secuencial.

* Fichero de Acceso Directo:

- Almacenamiento en dispositivo direccionable.


direccionable.
- Existe en los registros un campo denominado campo clave
que hace referencia inequívoca a dicho registro a través del
número de registro.
- Los datos están almacenados en el orden en el que han sido
introducidos.
- El acceso a los registros puede ser tanto aleatorio a
través del campo clave como secuencial
secuencial.
.

* Fichero de Acceso Indexado:

- Almacenamiento en dispositivo direccionable.


direccionable.
- Existe en los registros un campo denominado campo clave
principal y campo clave secundario, que hacen referencia
inequívoca a dicho registro.
- Los datos están almacenados en el orden alfabético por el
campo clave.
- El acceso a los registros puede ser tanto aleatorio a
través del campo clave como secuencial
secuencial.
.
- El acceso Indexado-Secuencial permite el acceso como si se
tratara de un fichero secuencial, sin embargo, los datos no
saldrán en el orden en el que fueron introducido
introducidos
s sino en
orden alfabético por el campo que estamos leyendo.

TEMA 5: Problemas y Algoritmos importantes

En este apartado vamos a ver una serie de algoritmos de gran


importancia en esta asignatura. El dominio de los siguientes
algoritmos nos da pie a enfrentarnos a una serie de algoritmos más
complejos y cuyo código depende de aquellos.

5.1 Ordenación por selección.

+-Algoritmo Orden_selec
Orden_selección
ción
|
| Constante entera n=...
| Vector entero a(1..n)
| Variable entera x,i,j,h,mayo
x,i,j,h,mayor
r
|
| Escribir "Introduzc
"Introduzca
a el vector a ordenar"
| +-Para h desde 1 hasta n hacer
| | Leer a(h)
| +-Finpara
|
| +-Para k desde 1 hasta n-1 hacer
| | mayor <- k
| | +-Para j desde k+1 hasta n hacer
| | | +-Si a(j)<a(i) entonces mayor <-j
| | | +-Finsi
| | +-Finpara
| | x <- a(j)
| | a(j) <- a(i)
| | a(i) <- x
| +-Finpara
| Escribir a
+-Final

5.2 Búsqueda Secuencial o Lineal.

+-Algoritmo Busqueda_se
Busqueda_secuencial
cuencial
|
| Constante entera n=...
| Vector entero a(1..n)
| Variable entera x,i,j,h,mayo
x,i,j,h,mayorr
|
| Escribir "Introduzca el valor a buscar"
| Leer x
|
| k <- 0
| +-Repetir
| | k <- k+1
| | +-Si a(k)=x entonces
| | | Escribir "El dato: ";a;
| | | "esta en la posición: ";k
| | +-Finsi
| +-Hasta que (k=n) or (a(k)=x)
+-Final

5.3 Búsqueda Dicotómica o Binaria.

+-Algoritmo Búsqueda_di
Búsqueda_dicotómica
cotómica
|
| Constante entera n= ...
| Vector de enteros a(1..n)
| Variable entera x,i,j,m,h
|
| Escribir "Introduzc
"Introduzca
a el vector a ordenar"
| +-Para h desde 1 hasta n hacer
| | Leer a(h)
| +-Finpara
|
| Escribir "Introduzca el valor a buscar"
| Leer x
|
| i <- 1
| j <- n
|
| +-Repetir
| | m <-(i+j) div 2
| | +-Si x < a(m) entonces
| | | j <- m-1
| | +-Sino
| | | i <- m+1
| | +-Finsi
| +-Hasta que (a(m)=x or i>j)
|
| +-Si i>j entonces
| | Escribir "El dato a buscar no se encuentra."
| +-Finsi
|
+-Final

5.4 Mezcla de ficheros Ordenados.

+-Algoritmo mezcla
|
| +-Registro estructurado r
| | variable cadena clave
| | ...
| +-Finregistr
+-Finregistro
o
|
| Ficheros de r A,B,C
| Variables enteras a,b
|
| Abrir secuencial A,B,C
| Iniciar lectura en A,B
| Iniciar escritura en C
|
| Leer (A,a)
| Leer (B,b)
|
| +-Mientras (NO fin(A) AND NO fin(B)) hacer
| | +-Si a.clave < b.clave entonces
| | | Escribir (C,a)
| | | Leer (A,a)
| | +ÄSino
| | | Escribir (C,b)
| | | Leer (B,b)
| | +-Finsi
| +-Finmientra
+-Finmientras
s
|
| +-Mientras (NO fin(A)) hacer
| | Escribir (C,a)
| | Leer (A,a)
| +-Finmientra
+-Finmientras
s
|
| +-Mientras (NO fin(B)) hacer
| | Escribir (C,b)
| | Leer (B,b)
| +-Finmientra
+-Finmientras
s
|
| Cerrar A,B,C
|
+-Final

TEMA 0: Introducción

0.1 Orígenes del C

El lenguaje C fue inventado por Dennis Ritchie en 1972


cuando trabajaba, junto con Ken Thompson, en el diseño del sistema
operativo UNIX.

El lenguaje C deriva del lenguaje B de Thompson, el cual, a


su vez, deriva del lenguaje BCPL desarrollado por Martin Richards.

Durante muchos años el estándar de C fue la versión


proporcionada
proporciona da con el sistema operativo UNIX versión 5. Pero pronto
empezaron a surgir muchas implementaciones
implementacio nes del C a raíz de la
popularidad creciente de los microordenadores.
microordenado res. Por este motivo, se
hizo necesario definir un C estándar que está representado
representad o hoy por
el ANSI C.

0.2 Características del lenguaje C

Algunas características del lenguaje C son las siguientes:

- Es un lenguaje de propósito general. Este lenguaje se ha utilizado


para el desarrollo de aplicacione
aplicaciones
s tan dispares como: hojas de
cálculos, gestores de bases de datos, compiladores
compiladores,
, sistemas
operativos, ...

- Es un lenguaje de medio nivel. Este lenguaje permite programar a


alto nivel (pensando a nivel lógico y no en la máquina física) y a
bajo nivel (con lo que se puede obtener la máxima eficiencia y un
control absoluto de cuanto sucede en el interior del ordenador).

- Es un lenguaje portátil. Los programas escrito s


escritos en C son
fácilmente transportables a otros sistemas.

- Es un lenguaje potente y eficiente. Usando C, un programador puede


casi alcanzar la eficiencia del código ensamblador junto con la
estructura del Algol o Pascal.

Como desventajas habría que reseñar que es más complicado de


aprender que
que otros lenguajes como Pascal
Pascal o Basic y que requiere
requiere una
una
cierta experiencia para poder aprovecharlo a fondo.

0.3 Uso del C

Los pasos a seguir desde el momento que se comienza a


escribir el programa C hasta que se ejecuta son los siguientes:

1.- Escribirlo en un editor.


2.- Compilarlo en un compilador.
3.- Enlazarlo en un enlazador.
4.- Ejecutarlo.

Paso 1: ESCRIBIRLO

El programa se puede escribir en cualquier editor que genere


ficheros de texto estándar, esto es, que los ficheros generados no
incluyan códigos de control y caracteres no imprimibles.

Estos ficheros que contienen código C se llaman ficheros


fuentes. Los ficheros fuentes son aquellos que contienen código
fuente, es decir, ficheros con texto que el usuario puede leer y que
son utilizados como entrada al compilador de C.

Los programas pequeños suelen ocupar un solo fichero fuente;


pero a medida que el programa crece, se va haciendo necesario
distribuirlo en más ficheos fuentes.

Paso 2: COMPILARLO

Elcompilador produce ficheros objetos a partir de los


ficheros fuentes. Los ficheros objetos son los ficheros que
contienen código objeto, es decir, ficheros con código máquina
(número binarios que tiene significado para el microprocesador)
microprocesa dor) y
que son utilizados como entrada al enlazador.
La extensión de estos ficheros es OBJ, aunque también los
hay con extensión LIB. A estos últimos se les llama también ficheros
de librería o biblioteca; contienen código máquina perteneciente a
código compilado suministrado por el compilador.

Paso 3: ENLAZARLO

El enlazador produce un fichero ejecutable a partir de los


ficheros objetos.

Los ficheros ejecutables son aquellos que contienen código


máquina y se pueden ejecutar directamente por el sistema operativo.

La extensión estos ficheros es EXE o COM.

Al proceso de enlazado también se le suele llamar el proceso


de linkado.

Paso 4: EJECUTARLO

El programa
p rograma se puede
pu ede ejecutar
e jecutar simplemente tecleando su
nombre desde la línea de comandos del sistema operativo.

ESQUEMA

Los pasos anteriores se resumen en el siguiente esquema:

f1.c ----> f1.obj ---------+


|
f2.c ----> f2.obj ---------|
|
. . |
. . |
. . |
|
fn.c ----> fn.obj ---------|
|--------------->
|---------------> f.exe
f1.lib ---------|
|
f2.lib ---------+
|
. |
. |
. |
|
fm.lib ---------+

Hoy día los compiladores


com piladores de C son muymu y sofisticados
sofis ticados e
incluyen entornos integrados desde los cuales editamos, compilamos,
enlazamos, y podemos realizar una multitud de servicios más.

En algunos de ellos se pueden realizar los pasos de


compilado, enlazado y ejecutado con la pulsación de una sola tecla.

En programación, la experiencia es el gran maestro. Por ello


es conveniente empezar a hacer programas en C cuanto antes.

TEMA 1 : Conceptos básicos

1.0 Introducció
Introducción
n
En este segundo tema se describirá la estructura básica de
un programa en lenguaje C así como la forma de visualizar distintos
tipos de datos en pantalla. Se introducirán los conceptos de tipos
de datos básicos y su utilidad.

1.1 El programa HOLA MUNDO

Este programa se ha convertido en un clásico dentro de los


libros de programación. Simplemente muestra en pantalla el mensaje
HOLA MUNDO, esto que puede parecer muy tonto es algo fundamental
puesto que si no sabemos imprimir mensajes ó datos en la pantalla
difícilmente nuestro programa se
se podrá comunicar con el usuario
usuario que
que
lo utilice.

Mostraremos el programa y a continuación describiremos cada


una de las instrucciones
instrucciones que lo forman.

/* Programa : HOLA MUNDO */


#include <stdio.h>

main()
{
printf ("\nHola mundo");
}

Como podemos observar se trata de un programa muy sencillo.


La primera línea es lo que se conoce como un comentario, un mensaje
que el programa añade al código del programa para explicar o aclarar
su funcionamiento o el de una parte de él. Los comentarios se pueden
situar en cualquier parte de nuestro código y se considerará como
comentarios cualquier mensaje que se encuentre entre los caracteres
/* y */.

Los "/*" y "*/" no son caracteres, sino símbolos o banderas.

La siguiente línea es lo que se conoce como directiva del


preprocesador,
preprocesad or, todos los compiladores de C disponen de un
preprocesador,
preprocesador, un programa que examina el código antes de compilarlo
y que permite
pe rmite modificarlo
mo dificarlo de cara
car a al compilador en distintos
disti ntos
sentidos. En temas posteriores trataremos en profundidad estas
directivas del preprocesador,
preprocesado r, pero para el desarrollo de los temas
anteriores a este debemos conocer al menos la directiva #include.
Las directivas se caracterizan
caracteriza n por comenzar con el carácter # y se
deben incluir al comienzo de la línea aunque es probable que esto
dependa de la implementación
implementac ión del compilador con el que estemos
trabajando. La directiva include permite añadir a nuestro código
algún fichero de texto, de tal forma que la directiva es sustituida
por el texto que contiene el fichero indicado. En general los
ficheros que acompañan a esta directiva son ficheros .H (Header -
Cabecera), en los que se incluyen definiciones de funciones que
deseamos utilizar en nuestros programas, constantes o tipos
complejos de datos.

La librería stdio.h (STandarD Input/Output)


Input/Outpu t) con tiene las
funciones estándar de entrada salida, y en ella se encuentra la
función printf que utilizamos en nuestro programa. Como se observa
en el código el nombre de la función a incluir debe ir entre los
caracteres <...>. A medida que vayan surgiendo iremos indicando las
funciones estándar que deberían
deberían incorporar todos los compiladores
compiladores C
y cual es su fichero de definición .H.
En la siguiente línea nos encontramos con main(). Esto
indica que aquí comienza nuestro programa, en realidad estamos
definiendo una función (esto se indica con los paréntesis al final
de la línea) pero esto lo discutiremos en temas posteriores. La
función main (principal en inglés) siempre debe existir y contendrá
el programa principal.

Finalmente nos encontramos el programa principal, una


sentencia printf entre llaves ({, }). Las llaves en C representan
bloques, y encierran un conjunto de sentencias o instrucciones (lo
que el computador ejecutará), considerando todas ellas como una
sola, permitiendo una definición homogénea de los distintos bloques
que constituyen
constituyen el programa. En nuestro caso
caso tenemos
tenemos un sólo
sólo bloque
que no es ni más ni menos que el programa principal, que en nuestro
caso está compuesto por una sola sentencia (la línea que contiene el
printf). Como se observa en el código las sentencias en C deben
terminar con un punto y coma (;), #include <stdio.h> y main() no son
sentencias, dan información sobre la estructura del programa, y por
tanto no finalizan con un punto y coma.

NOTA: La razón de que la línea "main()" no tenga un punto y


coma ";" al final es debido a que la sentencia en sí termina al
cerrar el corchete, no en que dicha línea proporcione información
sobre la estructura del programa. De hecho, si el "main()"
constituyese una línea de prototipo tendría un ";" al final.

La función printf permite visualizar datos formateados en


pantalla, es decir, permite indicar un formato como si de un impreso
o formulario se tratase indicando donde se deben visualizar cada
uno. En el siguiente tema cuando se introduzcan los tipos básicos de
datos se comprenderá mejor ésto. Por ahora sólo nos interesa conocer
que printf visualiza mensajes en pantalla.

El mensaje debe ir entre comillas dobles (") y dentro de las


comillas se puede mostrar cualquier secuencia de caracteres. El
formato de esta función para este segundo tema será:

printf ("mensaje");

En el siguiente tema, cuando expliquemos en profundidad la


instrucción, ampliaremos esta definición.

En nuestro programa observamos que el mensaje de texto que


visualiza la instrucción printf comienza con los caracteres \n.
Estos caracteres nos permiten algunas funciones especiales para
controlar la forma de visualizar los mensajes, la más utilizada en
\n que significa nueva línea, así nuestra sentencia printf ("\nHOLA
MUNDO"); moverá el cursor (la posición de la pantalla donde
actualmente se visualizan
visualizan los datos) a una nueva línea
línea situándolo
situándolo a
la izquierda de la pantalla y visualizará el mensaje HOLA MUNDO.

Para finalizar con este punto se indicará las secuencias


antecedidas por \ que se pueden incluir en una instrucción printf:

+-------------------------+------+
| Nueva línea | \n |
| Tabulador horizontal | \t |
| Tabulador vertical | \v |
| Backspace (<-) | \b |
| Retorno de carro | \r |
| Avance de página | \f |
| Pitido (alerta) | \a |
| Caracter \ | \\ |
| Caracter ? | \? |
| Caracter ' | \' |
| Caracter " | \" |
| Número Octal (ooo) | \ooo |
| Número Hexadecimal (hh) | \xhh |
+-------------------------+------+

Algunos comentarios sobre estos códigos. En primer lugar el


primer grupo (hasta carácter \), eran utilizados para mover el
cursor en terminales. Los terminales podían ser una pantalla o una
impresora, esta es la razón por la que nos encontramos cosas como
avance de página o retorno de carro. Los caracteres \ ? ' " son
especiales puesto que se utilizan dentro del mensaje a visualizar
para indicar como se visualiza, o sea, si escribimos \ el compilador
buscará el siguiente carácter y si es alguno de los anteriores los
visualiza sino corresponde con ninguno simplemente lo ignora, con lo
cual no podríamos visualizar el carácter \. Otro tanto sucede con
las comillas
comillas puesto que para C, las comillas representan una cadena
cadena
de caracteres, si escribimos " en nuestro mensaje se considerará que
éste termina ahí con lo que lo que se encuentre después no tendrá
sentido para el compilador.

Por último Número octal y Número hexadecimal nos permite


introducir directamente el código numérico en octal o hexadecimal
del carácter que deseemos visualizar, dependiendo del que esté
activo en nuestro computador. En general el código utilizado para
representar internamente los caracteres por los computadores
computadore s es el
código ASCII (American Standard Code for Information Interchange).

En este segundo tema hemos aprendido a escribir programas


que visualicen en pantalla mensajes utilizando la función estándar
de la librería stdio. Antes de terminar unos breves comentarios. El
lenguaje C diferencia entre mayúsculas y minúsculas con lo cual
main, MAIN, MaIn, serían identificadores
identificadores distintos para el
compilador, en general en C se suele escribir todo en minúsculas a
excepción de los mensajes a visualizar (cuyo uso depende del
programador)
programador ) y de las constantes, esto no tiene por que hacerse así
pero digamos que se trata de una tradición de la programación
programació n en C.
Las separaciones entre líneas también son arbitrarias y su única
función es facilitar la legibilidad del código, sirviendo de
separadores entre fragmentos de programa relacionados
relacionados entre sí. Y
esto es todo por ahora.

1.2 Tipos de datos básicos del C

La mayoría de los programas realizan algo útil y


generalmente para ello
ello es necesario trabajar con grandes
grandes cantidades
cantidades
de datos, si queremos realizar un programa que nos calcule un
sistema de ecuaciones tenemos que indicar cuales son las ecuaciones
para que el programa las resuelva. Por tanto un programa estará
constituido por una serie de datos y una serie de sentencias o
instrucciones
instrucciones que le dicen lo que tiene que hacer con esos datos.
Los lenguajes de programación
programació n disponen de una serie de tipos de
datos básicos,
básicos, y proporcionan herramientas
herramientas para crear
crear estructuras
estructuras a
medida que faciliten el acceso a la información. Así en nuestro caso
ficticio de resolver un sistema de ecuaciones podemos almacenar los
coeficientes
coeficiente s de cada ecuación con lo que utilizaríamos
utilizaríam os como tipo de
dato los números, si planteásemos el problema desde un punto de
vista matricial nos interesaría tener un tipo de datos matriz y lo
ideal sería tener un tipo de datos ecuación. En este apartado
describiremos
describirem os los tipos básicos que proporciona el lenguaje C y
dejaremos para temas posteriores la declaración
declaración de tipos complejos.

Estos tipos básicos son los siguientes:

+--------+---------------------------------------+
| int | Representa números enteros. |
| float | Representa números decimales. |
| double | Representa números decimales de mayor |
| | precisión. |
| char | Representa caracteres. |
+--------+---------------------------------------+

Aunque el tipo char represente caracteres internamente para


el computador no es más que un número comprendido entre 0 y 255 que
identifica un caracter dentro de el código especificado
especificado para tal
propósito en el sistema en el que nos encontremos trabajando. El
código más utilizado para este tipo de representación es el ASCII ya
mencionado anteriormente.

NOTA: Según la máquina, el compilador empleado y las opciones


de compilación activas, "char" puede interpretarse con signo o sin
signo. Esto es, de -128 a 127 o desde 0 a 255. Si se requiere una
representación
representac ión que no dependa de las opciones del compilador, etc.,
se puede poner "signed char" o "unsigned char", según el caso.

Como decíamos antes el ordenador debe de disponer de los


datos necesarios para resolver el problema para el que lo queramos
programar. Difícilmente se podría resolver un sistema de ecuaciones
si no se dispone de éstas. Para ello podemos definir variables. Las
variables almacenan valores de un tipo especificado
especificado y en ellas
almacenamos los datos de nuestro problema, se denominan variables
por que su valor puede cambiar a lo largo del programa. Para
referenciar una variable especificada es necesario que la podamos
identificar para ello se utiliza un nombre o identificador que no es
más que una secuencia de caracteres,
caracteres, esta secuencia no puede
contener caracteres españoles (acentos y eñes), ni caracteres que
tengan alguna función especial en el C como por ejemplo ej emplo los
caracteres que representan operaciones matemáticas +, -, etc...,
tampoco pueden contener espacios por lo que se suele utilizar el
carácter subrayado (_) si el identificador que deseamos asignarle a
nuestra variable está formado por varias palabras, pero en general
con los caracteres y números tenemos suficiente para dar nombres
autoexplicativos, aunque los números no pueden comenzar el nombre de
una variable. Las variables se suelen escribir con letra minúscula
aunque no es necesario y es aconsejable que los nombres sean auto
explicativos
explicativo s para que resulte más sencillo leer el programa (es
mejor llamar resultado a una variable que x).

Las variables se declaran indicando el tipo que van a tener


seguido de su identificador
identificador y terminando la línea con un punto y
coma. Algunos ejemplos:

int numero; /* número no sería un nombre válido */


float mi_variable
mi_variable;
;
char letra;

Si necesitamos declarar varias variables de un mismo tipo se


pueden incluir en la misma línea todos los nombres que deseemos
separándolos por una coma:

int numero1,numero2,numero3;
float coordenada_x,coordenada_y;

El compilador tiene que conocer las variables que va ha


utilizar cada bloque para reservarles sitio, por ello las variables
se suelen declarar al principio de cada bloque. Puesto que aún no
sabemos como definir funciones nuestro programa sólo dispone de un
bloque (el main()) con lo que nuestras variables deben de declararse
al comienzo del main() dentro del bloque, es decir, inmediatamente a
continuación de la llave abierta ({). Un ejemplo:

NOTA: Aunque el párrafo anterior da a entender que se puede


declarar una variable en cualquier momento, el estándar ANSI C
obliga a realizar las declaraciones
declaracione s al principio de cada bloque. En
el caso se variables globales la sintaxis es más flexible, para
poder utilizar el "Scope" en nuestro provecho.

main()
{
int numero;

numero =20;
}

Podemos también declarar variables fuera del bloque main().


Estas variables se conocen como variables globales y cualquier
función puede
puede acceder
acceder a ellas,
ellas, como sólo
sólo tenemos una función
función (main)
en este caso nos daría igual declarar las variables dentro o fuera
de main.

De poco nos servirían estos datos numéricos si no pudiésemos


realizar operaciones con ellos, el lenguaje C permite realizar las
operaciones básicas con estas variables de tipo numérico, estas son:

+---+---------------------------------------------+
| + | para indicar suma |
| - | para indicar resta |
| * | para indicar producto |
| / | para indicar división |
| % | para indicar módulo (resto división
división entera) |
+---+---------------------------------------------+

Podemos combinar estas operaciones en la forma que nos


plazca con variables o constantes (podemos operar variables con
números fijos) y utilizar los paréntesis, caracteres ( y ) para
indicar precedencia de las operaciones como lo haríamos en una
expresión matemática normal. En principio sólo podemos realizar
operaciones con variables que sean del mismo tipo, aunque en general
podemos operar los tipos float y double con tipos int o incluso
char, en principio no podríamos
podríamos almacenar un valor float (un número
real) en una variable int (entera),
(en tera), para
p ara ello
ell o tendríamos
tendr íamos que
convertir ese número
núme ro real en entero
ente ro de alguna forma. Podemos
convertir tipos básicos utilizando la facilidad del C conocida como
cast. Esta facilidad simplemente consiste en indicar antes de una
variable o constante el tipo al que lo deseamos convertir entre
paréntesis y el compilador se encargará del resto. Un ejemplo :

NOTA: El C también define la conversión automática de tipos.


float a;
int b;

b=30;
a=(float)b;

Para ejemplificar todo esto vamos a realizar un programa que


nos calcule el espacio recorrido por un móvil con velocidad uniforme
durante un tiempo determinado. El programa sería algo así:

#include <stdio.h>

main()
{
float e,v,t;

v = 30; /* Velocidad del móvil en Km/h */


t = 5; /* Tiempo durante el cual se mueve */

e = v*t;
printf ("\nVelocidad : %f\nTiempo : %f",v,t);
printf ("\nEspacio recorrido : %f",e);
}

Este programa calcula el espacio recorrido por un móvil en


movimiento uniforme a una velocidad indicada por la variable v,
durante un tiempo indicado por la variable t. Lo más interesante de
este programa es que hemos utilizado la función printf para
visualizar valores de variables.

Como decíamos más arriba la función printf permite


visualizar mensajes formateados,
formateados , es decir, en la cadena de
caracteres entre comillas dentro del paréntesis nos indica la forma
en la que se visualizarán
visualizarán los datos. Ya hablamos de los caracteres
especiales como \n, ahora nos ocuparemos de la visualización
visualizaci ón de las
variables.

Cuando deseamos visualizar una variable debemos indicarlo en


la cadena de formateo (la secuencia de caracteres entre comillas)
mediante el caracter % seguido de un caracter que nos indica el tipo
de dato a visualizar. Estos tipos son los siguiente:

+-----+------------------------------------------------+
| d,i | Entero en notación decimal con signo. |
| o | Entero notación octal sin signo. |
| x,X | Entero en notación hexadecimal sin signo. |
| u | Entero en notación decimal sin signo. |
| c | Entero como caracter simple. |
| s | Cadena de caracteres. |
| f | Tipo double (ó float) de la forma [-]mmmm.ddd. |
| e,E | Tipo double (ó float) en notación científica o |
| | exponencial [-]m.dddde+-
[-]m.dddde+-xx
xx ó [-]m.ddddE+-
[-]m.ddddE+-xx.
xx. |
| p | Tipo puntero. |
+-----+------------------------------------------------+

Podemos así mismo indicar el número de cifras a visualizar,


intercalando entre el % y la letra que identifica el tipo de dato un
número. Si indicamos dos números separados por un punto, el primer
número indica el número total de caracteres a visualizar y el
segundo el número de cifras decimales que queremos que se muestren,
obviamente este formato sólo se utilizará con tipo float o double.
Algunos ejemplos:

printf ("%f",numero
("%f",numero);
);
Visualiza un número real en el formato
formato normal, parte entera
entera
y parte decimal separadas por un punto.

printf ("%5.2f",num
("%5.2f",numero);
ero);
Visualiza un número entero en el mismo formato que la
anterior, pero sólo visualizando 5 cifras y siendo dos de ellas
reservadas para la parte decimal.

Hasta ahora hemos visto como decir a la función printf el


formato en el
el que debe visualizar los datos, pero no le hemos
hemos dicho
que datos debe visualizar.
visualizar. Lo que se escribirá
escribirá en el
el lugar indicado
por el % está especificado
especificad o a continuación de la cadena de formateo
entre comillas, separando cada una de ellas por comas. El primer %
coincide con el primer parámetro después de las comillas, el segundo
con el segundo y así sucesivamente.
sucesivament e. De esta forma un programa como
este:

#include <stdio.h>

main()
{
int i;
float a,b;

i = 10;
a = 30.456;
b = 678.12;

printf ("\nvar1:%d var2:%6.2f var3:%6.1f",i,a,b);


}

tendrá como salida:

var1:10 var2:30.46 var3:678,1

Como se puede observar


o bservar en el ejemplo si la precisión
especificada
especificad a en la cadena de formateo es menor que la real del
número, la función printf aproxima a la precisión especificada.
especificada.

1.3 Entrada de datos por teclado

El programa anterior para el cálculo de el espacio funciona


correctamente,
correctamen te, pero cada vez que deseemos calcular un nuevo espacio
debemos dar valores a las variables v y t y recompilar nuestro
programa. Sería estupendo poder leer por teclado los datos de la
velocidad y del tiempo y así permitir a nuestro programa trabajar
con cualquier valor de velocidad y tiempo. Aquí es donde realmente
se comprende de donde viene el nombre de variables.

Para leer los datos por teclado se utiliza la función scanf


cuya definición se encuentra también en el fichero stdio.h. El
formato es idéntico al de printf utilizando una cadena de formateo
con los caracteres % y la letra que indica el tipo, ahora del dato
que vamos a leer, seguido de la
la variable
variable en la que
que deseamos
deseamos que se
lea antecedida por el carácter &. Nuestro programa del móvil se
convierte ahora en:
#include <stdio.h>

main()
{
float v,t,e;

printf ("\nDime la velocidad de el móvil:");


scanf ("%f",&v);
printf ("\Dime el tiempo :");
scanf ("%f",&t);
e = v*t;

printf ("\nUn móvil desplazándose a %5.2f Km/h durante


%5.2f horas, recorre una distancia de %5.2f Km",v,t,e);
}

Ahora cada vez que el programa se ejecute nos pedirá que


introduzcamos
introduzcamos los valores de la velocidad y el tiempo y nos
proporcionará
proporciona rá el espacio que recorreríamos,
recorreríamos , evitando de esta forma
la necesidad de recompilar el programa cada vez que deseemos
realizar un nuevo cálculo.

TEMA 2: Control de Flujo de programa

2.0 Introducció
Introducción
n

En tema anterior aprendimos a trabajar con variables, leer


sus valores
va lores por
po r teclado,
tecla do, visualizarlas
vis ualizarlas en pantalla
pan talla y realizar
operaciones elementales con ellas.

Los programas que escribimos


e scribimos hasta ahora se ejecutaban
secuencialmente,
secuencialm ente, es decir, instrucción tras instrucción, sin
posibilidad de volver a ejecutar una instrucción ya ejecutada o
evitar la ejecución de un grupo de instrucciones
instruccione s si se dan unas
características determinadas.

En este tercer tema se describirán las instrucciones que nos


permiten escribir programas que no se ejecuten de una forma
secuencial en el sentido explicado en el párrafo anterior.

2.1 Expresiones condicionales.

En ciertas ocasiones nos puede interesar que un programa


llegado a un punto de su ejecución vuelva hacia atrás y se ejecute
de nuevo, pero lo que en general nos interesará será que este
regreso a una
una línea de terminada de nuestro código se realice
realice si se
se
cumple una cierta condición. Por esta razón es necesario explicar,
antes de comenzar
comen zar con las instrucciones
ins trucciones propiamente dichas de
control de flujo de programa, como le indicamos al ordenador que
deseamos evaluar una condición.

Las expresiones que nos permiten realizar ésto reciben el


nombre de expresiones condicionales
condicional es o booleanas. Estas expresiones
sólo pueden tomar dos valores: VERDADERO (TRUE) o FALSO (FALSE). En
general un valor de 0 indica que la expresión es falsa y un valor
distinto de 0 indica que la expresión es verdadera.

Como hemos indicado se trata de expresiones condicionales,


condicionales, y
análogamente
análogament e a las expresiones aritméticas podemos comparar
variables entre sí, constantes entre sí (lo cual no es muy útil
puesto que si conocemos los dos valores ya sabemos la relación que
existe entre ambas constantes) y por supuesto variables y
constantes. Además podemos agrupar condiciones entre sí formando
expresiones más complejas y ayudarnos de los paréntesis para indicar
el orden de evaluación. Los operadores condicionales son:

== Representa igualdad.
!= Representa desigualdad
> Mayor que.
< Menor que.
>= Mayor o igual que.
<= Menor o igual que.

Podemos encadenar distintas expresiones


expresiones condicionales,
condicionales, las
cuales deben de ir entre paréntesis (comparamos de dos en dos)
utilizando los operadores:

&& Y lógico.
|| O lógico.

Veamos un ejemplo de expresión condicional

(a==2)||((b>=0)&&(b<=20))

la expresión será cierta si la variable a es igual a dos o


si la variable b tiene un valor comprendido entre 0 y 20.

2.1.1 La instrucción if... else.

En inglés if significa si condicional, por ejemplo, si


llueve me llevaré el paraguas, else significa sino, sino llueve me
iré a la playa. Este es el significado que poseen en programación.
Su sintaxis es:

if (condición
(condición)
) instrucción;
instrucción;else
else instrucción;

NOTA: La sintaxis real


real del IF es la siguiente:
siguiente: if (condición)
(condición)
bloque else bloque.

Un programa
p rograma ejemplo nos indicará su funcionamiento
f uncionamiento con
claridad. Supongamos
Supongamos que deseamos dividir dos números. El número por
el que dividimos no puede ser cero, ésto nos daría un valor de
infinito, provocando un error en el ordenador. Por tanto antes de
dividir deberíamos de comprobar si el divisor es cero. El programa
sería algo como ésto:

#include <stdio.h>

main()
{
float dividendo,divisor;

printf ("\nDime el dividendo:")


dividendo:");
;
scanf ("%f",÷ndo)
("%f",÷ndo);
;
printf ("\nDime el divisor:");
scanf ("%f",&divi
("%f",&divisor);
sor);

if (divisor==0)
printf ("\nNo podemos dividir un número por 0");
else
printf ("\nEl resultado es: %f",dividendo/divisor);
}

Como en todas los comandos del lenguaje C una instrucción,


instrucción,
en general, puede ser solamente una o un conjunto de ellas incluidas
entre llaves.

Por último el lenguaje C dispone de un operador ternario (de


tres elementos) que permite construir determinadas estructuras
if-else, en concreto toma un valor u otro dependiendo de una
expresión condicional.
condicional. Su sintaxis es:

exp1 ? exp2 : exp3

Si exp1 es cierta la expresión tomará el valor exp2, sino


tomará el valor exp3. Un ejemplo de su utilización:

/* La variable z toma el valor máximo entre a y b */


z = ( (a>b) ? a : b);

Como se puede observar se trata de una secuencia if else


pero muy concreta, probablemente
probablement e el compilador generará un código
mucho más eficiente para este tipo de secuencia de ahí su inclusión
en el juego de operadores del C.

A continuación se describirán las instrucciones


instruccione s que nos
permiten controlar el flujo de programa, en las cuales tendremos que
utilizar expresiones condicionales
condicionale s continuamente,
continuament e, por lo cual no
insistiremos más en este tema.

2.2 Control del flujo de programa

2.2.0 Introducció
Introducción
n

A estas alturas el lector ya debería conocer lo que es el


flujo de programa. El flujo de programa es la secuencia de
instrucciones
instrucciones que un programa ejecuta desde su comienzo hasta que
finaliza. En principio la ejecución es secuencial, comienza con la
primera instrucción y termina con la última. Sin embargo es común
que nos interese que nuestro programa no termine con la última de
las instrucciones
instruccione s (si por ejemplo no podemos abrir un fichero y la
función del programa es modificar ese fichero, el programa no
debería realizar ninguna operación y terminar al detectar el error),
o puede que nos interese que un grupo de instrucciones
instruccion es se ejecute
repetidamente
repetidamen te hasta que le indiquemos que pare. Todo esto se puede
conseguir con las instrucciones que se describirán a continuación.

2.2.1 Creación de bucles de ejecución.

2.2.1.0 Concepto de bucle

En la introducción
introducción ya se ha mencionado lo que es un bucle.
Una secuencia de instrucciones
instruccione s que se repite un número determinado
de veces o hasta que se cumplan unas determinadas condiciones.

Los bucles
bucles son extremadamente
extremadamente útiles en nuestros
nuestros programas,
programas,
algunos ejemplos son:
* Lectura/Visualización
Lectura/Visualización de un número determinado de datos,
como por ejemplo una matriz.

* A veces se hace necesario introducir esperas en nuestros


programas ya sea por trabajar con un periférico lento o
simplemente por ralentizar su ejecución. Los primeros se
llaman bucles de espera
es pera activa y los
lo s segundo
seg undo bucles
vacíos.

* En aplicaciones gráficas como trazado de líneas o


rellenado de polígonos.

* Lectura de datos de un fichero...

A continuación
c ontinuación describiremos
describirem os las
la s opciones
op ciones que nos
proporciona el lenguaje de programación C para crear y gestionar los
bucles.

2.2.1.1 Bucle for

La primera opción de que disponemos es el bucle for. Este


tipo de instrucción se halla presente en la mayoría de los lenguajes
de programación estructurados,
estructurad os, y permite repetir una instrucción o
conjunto de instrucciones un número determinado de veces. Su
sintaxis es como sigue:

for (exp1;exp2;
(exp1;exp2;exp3)
exp3) instrucción
instrucción;
;

exp1 es una expresión que sólo se ejecuta una vez al


principio del bucle. El bucle for suele utilizarse en combinación
con un contador.
contador. Un contador
contador es una variable
variable que lleva la cuenta de
de
las veces que se han ejecutado las instrucciones sobre las que actúa
el comando for. Por tanto exp1 suele contener una expresión que nos
permite inicializar ese contador generalmente a 0 aunque eso depende
de para qué deseemos utilizar el bucle.

exp2 es la expresión que nos indica cuando debe finalizar el


bucle, por tanto
tan to se tratará de una expresión condicional. Su
interpretación
interpretac ión sería algo como; repite la instrucción (o
instrucciones)
instruccion es) mientras se cumpla exp2. Esta expresión se evaluará
en cada ciclo del bucle para determinar si se debe realizar una
nueva iteración.

NOTA: Hay que recordar que exp2 se evalúa al principio del


bucle, y no al final. Por tanto es posible no ejecutar el bucle
NINGUNA vez.

exp3 es una expresión que se ejecuta en cada iteración.


Puesto que como ya indicamos el bucle for se utiliza junto a un
contador, exp3 en general contiene una instrucción que actualiza
nuestro contador.

Por tanto en un bucle con contador distinguimos tres partes


diferenciadas:

* La inicialización del contador (exp1).

* La condición de fin de bucle (exp2).


* Y la actualización del contador (exp3).

El bucle for esta especialmente pensado para realizar bucles


basados en contadores. Se puede utilizar en bucle del tipo "repite
esto hasta
h asta que se pulse
puls e una tecla", pero para
pa ra estos
esto s tenemos
instrucciones
instruccion es más apropiadas. Veamos unos ejemplos que nos permitan
comprender más fácilmente el funcionamiento del comando for.

Ejemplo 1: Contar hasta diez.

#include <stdio.h>

main()
{
int i; /* Esta variable la utilizaremos como contador*/

for (i=0;i<10;i++) printf ("\n%d",i);


}

Este programa mostrará en pantalla numeros de 0 a 9 (diez en


total). exp1 inicializa nuestro contador que en este caso es una
variable de tipo entero, con el valor 0, exp2 nos dice que nuestra
instrucción (la función printf) debe repetirse mientras el contador
sea menor que diez y finalmente exp3 indica que el contador debe de
incrementarse
incrementarse en una unidad en cada ciclo del bucle.

Nos podría interesar contar desde diez hasta 1, en este caso


el comando for debería de ser:

for (i=10;i>0;i--) printf ("\n%d",i);

Ejemplo 2: Visualizar dos tablas de multiplicar en pantalla.

#include <stdio.h>

main()
{
int i;
int tabla1,tabla2;

tabla1 = 2; /* Primero la tabla del dos */


tabla2 = 5; /* y a continuación la tabla del cinco*/

for (i=1;i<11;i++)
(i=1;i<11;i++)
{
printf ("\n %2dx%2d=%3d",tabla1,i,tabla1*i);
printf (" %2dx%2d=%3d",tabla2,i,ta
%2dx%2d=%3d" ,tabla2,i,tabla2*i);
bla2*i);
}
}

El ejemplo
ejem plo es análogo al anterior,
anter ior, pero
pe ro en este caso
visualizamos
visualizamo s valores desde uno a diez, en lugar de visualizarlos
visualizarlo s de
0 a 9. En este ejemplo, el bucle actúa sobre un conjunto de
instrucciones,
instrucciones, no sobre una sola, por tanto debemos introducirlas
introducirlas
entre las llaves para indicar al compilador que la instrucción for
actúa sobre las dos instrucciones.
instruccione s. Estamos considerando todo lo que
se encuentre entre las llaves como una sola instrucción.

Para terminar con los bucles de tipo for un leve comentario


sobre los bucles añadados,
añadados, simplemente
simplemente lo que se hace es incluir un
un
bucle dentro de otro. Supongamos que deseamos introducir los datos
de nuestro jugadores preferidos por teclado para almacenarlos
almacenarlo s en el
ordenador, y que de cada jugador queremos almacenar por ejemplo, su
nacionalidad,
nacionalida d, su peso y su altura. En este caso nos sería útil un
bucle anidado. El bucle exterior nos contaría jugadores, mientras
que para cada jugador tendríamos otro bucle que nos leyera los tres
datos que necesitamos.
nece sitamos. Si tenemos
tene mos veinte
vein te jugadores
juga dores preferidos,
incluiríamos una unas instrucciones como estas:

for (i=0;i<20;i++)
(i=0;i<20;i++)
{
printf ("Jugador preferido %d",i);
for (j=0;j<3;j++)
{
leo característica j;
la almaceno donde sea;
}
}

Nada más en lo que a bucles de tipo for respecta. A


continuación
continuació n veremos las otras estructuras que nos proporciona el
lenguaje C para la realización de bucles.

2.2.1.2 Bucles while.

La sintaxis de este bucle será algo así:

while (exp2) instrucción;

En inglés while significa mientras, por tanto la línea


anterior significaría mientras de cumpla exp2 ejecuta la
instrucción. Obviamente la instrucción que ejecutemos debe de
permitir que en algún caso se cumpla exp2, de lo contrarío el
ordenador permanecería eternamente ejecutando instrucción.
instrucción . También
es evidente
evidente que exp2
exp2 debe ser una expresión condicional.
condicional. Como vemos
este tipo de bucles no está orientado a contadores, es mucho más
genérico, sin embargo
embargo se puede
puede utilizar
utilizar de forma análoga a for. Con
la nomenclatura utilizada anteriormente tendríamos algo como ésto:

exp1;
while (exp2)
{
instrucción;
exp3;
}

Con este esquema se hace patente la utilidad de la


instrucción for para bucles con contador puesto que se "centraliza"
todo el proceso de gestión del contador (inicialización
(inicializac ión y
actualización)
actualizaci ón) en una sola instrucción. Un error común al escribir
un bucle con contador con una estructura while es olvidar introducir
exp3, con lo cual nunca se cumple exp2 y nunca salimos del bucle. De
nuevo un bucle infinito aunque a veces nos interesa tener un bucle
infinito. La forma más sencilla de realizar un bucle infinito es con
la expresión:

while (1) instrucción;

Como indicamos exp2 es una expresión condicional y para


estas expresiones un valor distinto de 0 es verdadero por tanto un 1
es siempre cierto y no hay manera de salir del bucle puesto que es
una constante y ninguna modificación de variables por parte de
instrucción tendría repercusiones sobre ella.

Los bucle while son útiles en aplicaciones como; lee datos


de este fichero mientras no llegues al final ó muestra estos datos
en la pantalla mientras no pulse una tecla.

Una peculiaridad de esta instrucción es que puede no


ejecutarse nunca la instrucción afectada por el while. Algunos
ejemplos:

Ejemplo 1: Contar hasta diez.

#include <stdio.h>

main()
{
int i;

i = 0;
while (i<10)
{
printf ("\n%d",i);
i++;
}
}

El primer valor que visualizaremos será el 0. Cuando i tenga


el valor nueve la condición i<10 todavía se cumple por lo que
entraremos en el bucle de nuevo, visualizaremos el nueve e
incrementamos
incrementam os i con lo que pasa a tener el valor 10 momento en el
cual se vuelve a evaluar la expresión i<10 que en este caso sería
falsa y no volveríamos a entrar en el bloque de instrucciones
instruccion es (las
que están entre llaves). Así visualizamos
visualizamos nueve número de 0 a 9 como
antes. Si incluyésemos una instrucción para visualizar el valor de i
antes de abandonar el programa (justo antes de las últimas llaves el
valor que se mostraría sería 10.

Ejemplo 2: Lee números enteros hasta que se introduzca el


valor hasta que se introduzca el valor 0.

#include <stdio.h>

main()
{
int numero;

numero = 10;
while (numero!=0)
{
printf ("\nDime un número:");
scanf ("%d",&numero);
}
}

En este ejemplo tenemos que introducir en la variable número


un valor distinto de cero antes de entrar en el bucle, puesto que en
principio al declarar una variable el valor de ésta no está
determinado y podría valer cero, en cuyo caso nunca se ejecutaría el
bucle. Esta es la misión de la instrucción numero = 10;.
2.2.1.3 Bucle do.. while

Su funcionamiento
funcionamien to es análogo al anterior, con la única
salvedad de que la condición ahora se evalúa después de ejecutar la
instrucción su sintaxis sería:

do instrucción while (exp2);

Si en el ejemplo anterior utilizamos esta estructura no


sería necesario actualizar numero con un valor distinto de cero,
puesto que antes de comprobar si es cero leeríamos el valor.

#include <stdio.h>

main()
{
int numero;

do
{
printf ("\nDime un numero :");
scanf ("%d",&numero);
} while (numero !=0);

La diferencia fundamental con la instrucción anterior es que


esta estructura ejecuta la instrucción sobre la que actúa al menos
una vez.

2.2.1.4 Modificadores del flujo de programa.

Puede que en ciertas


ciertas ocasiones
ocasiones no nos interese
interese que si se da
da
alguna condición sólo se ejecute un conjunto de todas las
instrucciones
instruccion es sobre las que actúa el bucle o simplemente salir del
bucle antes de llegar a la condición que hayamos indicado en el
comando for
for o en while. Esto es posible mediante
mediante dos
dos modificadores:
modificadores:
continue y break.

El primero de ellos, continue, permite volver a reevaluar la


condición de salida, es decir, después de ejecutar continue la
siguiente instrucción
instrucción que se ejecutará será el for o el while.
Veamos un ejemplo de aplicación para comprender mejor como funciona
este comando.

#include <stdio.h>

main()
{
int numero;
int contador;

contador =0;
do
{
printf ("\nIntroduce el número %2d:",contador);
scanf ("%d",&numer
("%d",&numero);
o);
if ((numero<0)||(numero>20)) continue;
contador++;
} while (contador<50);
}
Este programa lee números en una variable hasta un máximo de
50, alcanzado este máximo el programa termina. Además si el número
no está entre 0 y 20 (si es menor que 0 o mayor que 20) vuelve a
pedir que lo introduzcamos. El comando continue en la instrucción if
obliga al programa a saltar a la instrucción while donde se vuelve a
evaluar la condición, sin pasar por la línea en la que se incrementa
el contador. De esta forma se nos vuelve a pedir el mismo número y
la entrada incorrecta no es tenida en cuenta.

La función de break es ligeramente distinta no salta a la


instrucción en la que se evalúa la condición sino que simplemente
abandona el bucle y continúa la ejecución en la línea inmediatamente
siguiente al bucle.

#include <stdio.h>

main()
{
int i;

for (i=0;i<20;i++)
(i=0;i<20;i++)
{
if (i==5) break;
printf ("\n%d",i);
}
printf ("\n\n%d",i)
("\n\n%d",i);
;
}

La salida de este programa sería algo como esto:

0
1
2
3
4

Y con esto terminamos todo lo relacionado con los bucles en


lenguaje C. Los bucles son una estructura básica y es necesario
utilizarla en la inmensa mayoría de los programas.

2.2.2 Menús de Opciones.

2.2.2.1 Introducció
Introducción
n

La mayoría de los programas permiten realizar una serie de


operaciones sobre datos. Lo ideal sería poder indicarle al ordenador
que operación
op eración deseamos realizar sobre estos
es tos datos
dat os en lenguaje
natural. Por ejemplo, para una base de datos nos podría interesar
decirle al ordenador: "Buscame todas la fichas de libros que traten
sobre informática"
informática" ó "Borra de la base de datos el libro tal".
Existen en la actualidad
actualidad herramientas
herramientas de este tipo, pero
pero aún distan
distan
bastante del lenguaje natural, por ello una de las formas más
sencillas de indicar al ordenador la operación que deseamos realizar
es utilizar un menú de opciones.

La otra solución comúnmente adoptada es una línea de


comandos, es decir, escribimos una frase en un lenguaje muy reducido
indicando al ordenador lo que deseamos hacer (de una forma similar a
como lo hacemos en MS-DOS). Esta solución tiene la desventaja de
tener que aprender complicados comandos y distintas secuencias para
distintos programas.

Un menú nos muestra en pantalla todas las opciones que


podemos realizar con nuestro programa de forma que no es necesario
que el usuario conozca ninguna serie de ordenes complejas,
simplificando
simplifican do por tanto el uso de los programas por parte de los
usuarios.

2.2.2.2 Sentencia switch-case


switch-case-default
-default

La mayoría de los lenguajes de alto nivel disponen de alguna


instrucción que permite gestionar el valor de una variable de una
forma estructurada
estructurada y sencilla,
sencilla, permitiendo
permitiendo por tanto la creación
creación de
menús de programa
progr ama de una forma
fo rma sencilla.
senc illa. En
E n lenguaje
lengu aje C esta
instrucción es switch-case-default. Veamos un ejemplo de como
funciona mediante un pequeño programita.

#include <stdio.h>

main()
{
int opcion;

printf ("\nEjemplo de Menú de Programa");


printf ("\n1.-Cargar fichero de datos");
printf ("\n2.-Almacenar fichero de datos");
printf ("\n3.-Modificar datos");
printf ("\n4.-Salir");
("\n4.-Salir");

printf ("\n\nDime tu opción :");scanf ("%d",&opcion);

switch (opcion)
{
case 1:
/* Código para cargar fichero de datos*/
break;
case 2:
/* Código para almacenar datos */
break;
case 3:
/* Código para modificar datos */
break;
case 4:
/* Salir del programa */
exit (0);
default :
printf ("\nSu opción no está disponible");
printf ("\nInténtelo con otra");
}

Del ejemplo se deduce fácilmente el funcionamiento


funcionamie nto de esta
secuencia. El comando switch (var) realizará una bifurcación o salto
a la línea indicada por la variable var. Si var vale 2, el programa
se seguirá ejecutando a partir de la línea marcada con case 2. Todos
los separadores case están separador por un comando break, ya que de
no ser así la ejecución seguiría lineal hasta encontrar la llave que
termina el comando switch. La palabra clave default indica el código
que se debería ejecutar si el valor de la variable especificada en
el switch no corresponde con ninguno de los indicados por los case
dentro del switch. Así en nuestro ejemplo si opcion tiene un valor
distinto de 1, 2, 3 ó 4, se mostrará en pantalla un mensaje
indicando que la opción indicada no es correcta. La sintaxis de esta
estructura sería:

switch (variable)
{
case valor1-varia
valor1-variable:
ble:
código asociado;
case valor2-varia
valor2-variable:
ble:
código asociado;
.
.
case valorN-varia
valorN-variable:
ble:
código asociado;
default:
código asociado;
}

Dentro del código asociado a cada opción se deberán incluir


las instrucciones
instruccione s break adecuadas. Ya se explicó el funcionamiento
funcionamien to
de break y continue cuando se habló de bucles. Su funcionamiento
funcionamient o es
análogo para los comandos for, while, do-while, y switch.

Un fragmento de código para la imprementación


imprementac ión de un menú
completo sería:

while (Opcion!=0)
{
/* Secuencia de printfs que muestran en pantalla
el menú. En este caso la opción 0 debería ser salir */
switch (opcion)
{
/* Secuencia de cases */
default :
/* Mostrar mensaje de error */
}
}

Por su puesto las aplicaciones


aplicaciones del comando
comando switch van mucho
mucho
más allá de la simple creación de menús. Puede ser utilizada en
cualquier aplicación en la que se necesite realizar distintas
operaciones dependiendo de un valor. Un ejemplo sencillo podría ser
un un programa que imprimiese textos en impresora. Podríamos marcar
en el texto mediante una secuencia especial como debería ser impreso
el texto a continuación. Por ejemplo:

@N - Activa Negrita.
@n - Desactiva Negrita.
@C,@c - Activa/desactiva cursiva.
@S,@s - idem sibrayado
etc...

Leeríamos estos valores provenientes del teclado o de un


fichero y con algo de procesamiento
procesamiento y una instrucción switch con
dicha variable, en cada case enviaríamos a la impresora la secuencia
adecuada para realizar cada una de la opciones.
TEMA 3 : Estructuras de datos estáticas

3.0 Introducció
Introducción.
n.

En este tema se describirán las herramientas que proporciona


el lenguaje C para trabajar con tipos y estructuras de datos,
flexibilizando
flexibiliza ndo de esta forma la creación de programas por parte del
programador.

3.1 Matrices Estáticas.

La matriz es una estructura de datos básica dentro de los


lenguajes de programación
programación y conceptualmente
conceptualmente son identicas a sus
homónimas matemáticas.
matemáticas. Por tanto
tanto una matriz es
es un conjunto
conjunto de datos
de un tamaño definido que se encuentran consecutivos en memoria y en
la que es posible el acceso al elemento que deseemos simplemente con
indicar su posición. La declaración de una matriz en lenguaje C es
como sigue:

tipo_de_dato
tipo_de_dato identificado
identificador[tamaño1][
r[tamaño1][tamaño2]...;
tamaño2]...;

Dónde :

tipo_de_dato: Es el tipo de datos que contendrá la matriz.


Hasta ahora sólo conocemos los tipos básicos de datos; int, float,
double, char. Posteriormente veremos como definir nuestros propios
tipos de datos.

identificador: Es el nombre que le damos a la variable


matriz y po el cual la referenciaremos
referenciaremos en nuestro programa.

[tamaño] : Indica el número de elementos de tipo


tipo_de_datos contendrá la matriz identificador. Si definimos dos
tamaños [tamaño1][tamaño2]
[tamaño1][tamaño2] nuestra matriz será bidimensional.

Algunas declaraciones de matrices serían:

/* Matriz de números reales de 10x10 */


float matriz[10][10];
/* Matriz tridimensional de números enteros 20x20x10 */
int Tridimensional[20][20][10];

Como ya se supondrá el acceso a cada elemento de la matriz


se realiza especificando su posición, pero ésta comienza a contarse
desde el valor 0, es decir, la primera matriz que hemos definido
(matriz) tendrá elementos desde el [0][0] al [9][9]. Esto puede
causar algunos mal entendidos cuando se trabaja con matrices
estáticas. Por ejemplo:

a = matriz [2][1];
/* A toma el valor del elemeto (2,1) comenzando a contar
desde 0 o del (3,2) si consideramos que el primer valor de la matriz
es el (1,1) */

tridimensional [5][16][1] = 67;


/* Introduce
Intr oduce el
e l valor 67 en la ent rada
entrada de la matriz
especificada */

Las variables de tipo matriz como el resto de las


declaraciones,
declaracion es, se pueden inicializar en el momento de su
declaración,
declaración , ayudándose de las llaves ({}) para la inclusión de
inicializaciones
inicializaciones múltiples.

int matriz[2][3] = {
{ 1,2,3 },
{ 4,5,6 }
};

Estas líneas nos declararían una matriz llamada "matriz" de


2x3 elementos inicializada
inicializad a con los valores indicados. Las matrices
son extremadamente
extremadamen te útiles para trabajar con multitud de problemas
matemáticos que se formulan de esta forma o para mantener tablas de
datos a los
los que se accede
accede con frecuencia y por tanto su referencia
tiene que ser muy rápida. Supongamos que estamos desarrollando
desarrolland o un
programa para dibujar objetos en tres dimensiones y más que la
exactitud de la representación
representac ión (aunque esto es muy relativo), nos
interesa la velocidad. En la representación de objetos
tridimensionales
tridimensio nales se hace continua referencia a las funciones
trigonométricas
trigonométr icas seno y coseno. El cálculo de un seno y un coseno
puede llevar bastante tiempo así que antes de comenzar la
representación
representac ión calculamos todos los senos y cosenos que necesitemos
(por ejemplo con una resolución de 1 grado -360 valores-) cada vez
que necesitemos
necesitemos uno de estos valores accedemos a la matriz
matriz en lugar
de llamar a la función que nos lo calcula. Veamos como sería nuestro
programa (las funciones sin y cos se encuentran en la librería
estandar math.h y sus paramétros están en radianes).

#include <stdio.h>
#include <math.h>

main()
{
float senos[360]; /* Almacenamos senos */
float cosenos[360];
int i;

/* Inicializamos las matrices */


for (i=0;i<360;
(i=0;i<360;i++)
i++)
{
seno[i] = sin (3.14159*i/
(3.14159*i/180);
180);
coseno[i] = cos (3.14159*i/180);
}

printf ("\nEl coseno de 30 es : %f",coseno[30]);


printf ("\nEl seno de 30 es : %f",seno[30]);
%f",seno[30]);
}

3.2 Tipos compuestos

3.2.0 Introducció
Introducción
n

En muchas ocasiones nos interesaría disponer de variables


compuestas de otras variables y trabajar con ellas como si se
tratasen de una sola. Un ejemplo típico es una ficha de datos de una
agenda. Necesitaríamos
Necesitaríam os una variable que nos almacenase el nombre,
otra variable que nos almacenase la dirección, otra para el teléfono
y así sucesivamente para todos los datos que deseemos mantener.
Podríamos disponer de una variable
variable para
para cada campo
campo (cada una de las
informaciones
informacion es que componen nuestra ficha) pero esto resultaría un
tanto engorroso a la hora de su programación.
El lenguaje
l enguaje C dispone
dis pone de mecanismos
mec anismos para trabajar con
variables compuestas de otras variables con suma facilidad. Existen
dos tipos básicos: estructuras y uniones.

3.2.1 Estructuras de datos.

Se trata de la forma más versatil de trabajar con fichas de


información.
información . Veamos como se definen y posteriormente
posteriormen te comentaremos
todos los aspectos relevantes de ellas.

struct [Nombre_de_la_estructura
[Nombre_de_la_estructura]
]
{
tipo1 campo1;
tipo2 campo2;
.
.
tipoN campoN;
} [variable];

La palabra
palabra clave struct define una
una estructura.
estructura. Por tratarse
de un tipo de datos puede utilizarse directamente para definir una
variable. La variable aparece entre corchetes puesto que puede ser
omitida. Si se especifica una variable, estaremos definiendo una
variable cuyo tipo será la estructura que la precede. Si la variable
no es indicada definimos un nuevo tipo de datos (struct
Nombre_de_la_estructura)
Nombre_de_l a_estructura),, que podremos utilizar posteriormente.
posteriorme nte. Si
es el nombre de la estructura lo que se omite, tendremos que
especificar obligatoriamente
obligatoriamente una variable que tendrá
tendrá esa estructura
estructura
y no podremos definir otras variables con esa estructura sin tener
que volver a especificar todos los campos. Lo que se encuentra
dentro de las llaves es una definición típica de variables con su
tipo y su identificador.
identificado r. Todo esto puede parecer un poco confuso
pero lo aclararemos con unos ejemplos.

struct punto
{
float x;
float y;
int color;
} punto_de_fuga;

Aquí estamos definiendo una variable llamada punto_de_fuga


cuyo tipo es una estructura de datos formada por tres campos y a la
que hemos llamado punto. Dos de ellos son de tipo float y
representan las coordenadas del punto, el tercer valor es un entero
que indica el color de ese punto. En este caso hemos definido una
variable y una estructura. Al disponer de un identificador para esta
última podemos definir nuevas variables de esta estructura.

struct punto origen1;


struct punto final1;

Donde origen1 y final1 son variables de tipo struct punto


que hemos definido anteriormente.
anteriorment e. Si en la definición de
punto_de_fuga
punto_de_fu ga no se hubiese
hub iese incluído
in cluído un
u n identificador
ident ificador para la
estructura (en este caso el identificador es punto), no podríamos
definir nuevas variables con esa estructura ya que no estaría
identificada por ningún nombre.
También podríamos haber excluído el nombre de la variable
(punto_de_fuga).
(punto_de_f uga). En este caso lo que definiríamos
definiríamo s sería una
estructura llamada punto que pasaría a ser un nuevo tipo disponible
disponible
por el usuario. Así los tipos de variables de que dispondríamos
ahora serían:

int
float
double
char
struct punto

Por tanto podríamos definir cualquier variable con estos


tipos o incluso definir matriz de estos tipos.

struct punto matriz_de_puntos[30];


matriz_de_pu ntos[30];

Así estaríamos definiendo una matriz de 30 elementos en la


que cada elemento es una struct punto con sus tres campos.

Lo que ahora nos interesa es saber como referenciar esos


campos y acceder o modificar, por tanto la información que
contienen. Esto se consigue separando el identificador
identificad or del campo de
la variable mediante un punto. Así:

punto_de_fuga.x = 0;
punto_de_fuga.y = 0;
punto_de_fuga.color = 10;

inicializa la cada uno de los campos de la variable punto de


fuga con sus valores correspondientes. Está claro que para acceder a
los campos necesitamos alguna variable cuyo tipo sea nuestra
estructura. Si no tenemos variable no tenemos información (sería
como hacer int = 6).

En el caso de la matriz tenemos tantas variables de tipo


struct punto como las indicadas, puesto que el punto separa el
nombre de la variable del campo al que queremos acceder, la forma de
modificar una entrada de la matriz sería:

matriz_de_puntos[4].x = 6;

matriz_de_puntos.x[4] = 6; /* No sería correcto */

Esta última declaración se podría utilizar con una


estructura de un tipo como:

struct otra
{
float x[10];
} matriz_de_puntos;

Con lo cual accederíamos


accederíamo s al cuarto elemento del campo x de
matriz_de_puntos que es una variable de tipo struct otra constituida
por una matriz de diez floats.

Para terminar con la declaración struct indicar que es


posible la declaración de estructuras anidadas, es decir, un campo
de una estructura puede ser otra estructura.

struct vector
{
float x;
float y;
float z;
};

struct poligono_cuadrado
poligono_cuadrado
{
struct vector p1;
struct vector p2;
struct vector p3;
struct vecto p4;
};

struct cubo
{
struct poligono_cua
poligono_cuadrado
drado cara[6];
int color;
struct vector posicion;
};

struct cubo mi_cubo;

Hemos declarado una variable (mi_cubo) de tipo struct cubo


que es una estructura conteniendo un valor entero que nos indica el
color de nuestro objeto, una variable de tipo struct vector
(posicion) indicando la posición del objeto en un espacio de tres
dimensiones (posicion tiene tres campos x,y,z por tratarse de una
struct vector) y una matriz de seis elemento en la que cada elemento
es un struct poligono_cuadrado,
poligono_cu adrado, el cual está formado por cuadro
vectores que indican los cuatro vértices del cuadrado en 3D. Para
aceder a todos los campos de esta variable necesitaríamos sentencias
del tipo.

mi_cubo.color = 0;
mi_cubo.posicion.x
mi_cubo.posicion.x = 3;
mi_cubo.posicion.y
mi_cubo.posicion.y = 2;
mi_cubo.posicion.z
mi_cubo.posicion.z = 6;
mi_cubo.cara[0].p1.x
mi_cubo.cara[0].p1.x = 5;
/* Ahora acedemos a la coordenada 0 del tercer polígono
de la cara 0 de mi_cubo*/
mi_cubo.cara[0].p3.z
mi_cubo.cara[0].p3.z = 6;
....

3.2.2 Estructuras solapadas. union

La definición de una union es analoga a la definición de una


estructura. La diferencia entre ambas es que los campos que
especifiquemos
especifique mos en una union ocupan todos la misma posicion de
memoria. Cuando se declara una union se reserva espacio para poder
almacenar el campo de mayor tamaño de los declarados y como ya se
dijo todos los campos ocupan la misma posición en la memoria. Veamos
un ejemplo.

union ejemplo
{
char caracter;
int entero;
} mi_var;
mi_var es una variable cuyo tipo es union ejemplo, y el
acceso a cada campo de los definidos se realiza igual que en las
struct mediante la utilización de un punto. Hasta aquí nada nuevo lo
que sucede es que carácter y entero (los dos campos) ocupan la misma
posición de memoria. Así:

mi_var.entero = 0; /* Como el tipo int ocupa más que el tipo


char ponemos a 0 toda la union */
mi_var.caracter
mi_var.carac ter = 'A'; /* El código ASCII de A es 65, por
tanto ahora mi_var.entero = 65 */
mi_var.entero = 0x00f10;

Esta última instrucción introduce un valor en hexadecimal en


la variable mi_var.entero. El código hexadecimal se representa en C
anteponiendo al número los caracteres 0x. Para comprender lo que
realiza esta instrucción veamos un poco como el ordenador representa
representa
los número internamente.

Todos hemos oido alguna vez que el ordenador sólo entiende


ceros y unos, pues bien, lo único que significa ésto es que el
ordenador cuenta en base dos en lugar de hacerlo en base diez como
nosotros. Cuando contamos en base diez comenzamos en 0 y al llegar a
nueve añadimos una unidad a la izquierda para indicar que llegamos a
las centenas y así consecutivamente.
consecutivamente. Cada cifra de un número en base
diez representa esa cifra multiplicada por una potencia de diez que
depende de la posición del dígito. Es lo que se llama descomposición
factorial de un número.

63452 = 6*10^4+3*10^3+4*10^2+5*10^1+2*10^0=
= 60000+3000+4
60000+3000+400+50+2
00+50+2

Como nuestro ordenador en lugar de contar de diez en diez


cuenta de dos en dos cada cifra es una potencia de dos. El sistema
de numeración en base dos se denomina sistema binario.

b100101 = 1*2^5+0*2^4+0*2^3+1*2^2+0*2^1+1*2^0=
= 32 + 0 + 0 + 4 + 1 = 37

Así es como representa el ordenador el número 37 en su


sistema binario. Cada una de las cifras de un número binario se
denomina BIT (BInary digiT) y los ordenadores los suelen agrupar el
grupos de 8. Así 8 bits se denomina un byte, 16bits serían 2 bytes y
se denomina word o palabra y así sucesivamente.

El mayor número que podríamos representar en binario con 1


byte (8bits) sería:

b11111111 = 255

Este es el tamaño que el lenguaje C asigna al tipo char, que


sólo puede representar 256 valores distintos, desde 0 a 255. El tipo
int short suele ocupar una palabra es decir, 16 bits. Así con 16
bits el mayor número que podemos representar es:

b1111111111111111 = 65535

NOTA: El tamaño asociado a cada tipo de datos es muy


específico de cada compilador/ordenador.
compilador/o rdenador. No debería darse nada por
supuesto...

Los números en binario rápidamente


rápidamente se hacen muy largos por
ello se utilizan otros sistemas de numeración que permitan una
escritura más compacta sin perter la información binaria en gran
medida. Esto sistemas son en general sistemas con bases que son
potencias de dos. Así tenemos el sistema octal (base 8) y el sistema
hexadecimal (base 16). Este último es el más ampliamente usado,
disponemos de 16 cifras de 0 a F(15) y la característica
característ ica más
importante de este sistema es que cada cifra hexadecimal, representa
cuatro bits binarios, con lo cual el paso de un sistema al otro es
extremadamente
extremadamente fácil.

Volvamos ahora a la instrucción anteriormente indicada

mi_var.entero = 0x00f10;

Si pasamos este número a binario obtenemos:

0 -> 0000
f -> 1111 -> 15 en decimal
1 -> 0001 -> 1 en decimal

0f10 <-> 0000111100010000 -> 3856 en decimal

Como dijimos anteriormente


anteriormente un char ocupa 8 bits y un int
ocupa 16, como la union los solapa tendríamos un esquema en la
memoria del ordenador como éste:

int 000011110001 0000


0000111100010000 -> 3856
char 00010000 -> 65 ('A')

Así mi_var.caracter contendrá el valor A, pero mi_var.entero


contendrá el valor 3856.

NOTA: Como ya se indicó en la nota anterior, el tamaño


asignado a cada tipo depende del ordenador y del compilador. Además,
algunos ordenadores almacenan los números en formato Bajo/Alto (los
8 bits e Intel) y otros en formato Alto/Bajo (Motorola, Sparc,
etc.).

Este tipo de estructura se suele utilizar en aplicaciones a


bajo nivel en la que es necesario poder utilizar este tipo de
solapamiento
solapamient o de bits. Como
C omo ya se habrá
habr á podido
podid o comprobar
compr obar para
compremder mínimamente como funciona esto es necesario bajar mucho
al nivel de la máquina con la consiguiente
consiguiente complicación de la
explicación.

3.2.3 Tipos definidos por el usuario.

Con las palabras clave struct y union, podemos definir


nuevos tipos de variables pero tenemos que indicar estos tipos con
todo su nombre, es decir, struct mi_struct. El lenguaje C dispone de
un comando que nos permite dar el nombre que nosotros deseemos a
cualquier tipo de variable. El comando es typedef y su forma de
utilización es como sigue:

typedef tipo nuevo_tipo

Algunos ejemplos para aclarar las cosas:

typedef unsigned char BYTE;


typedef struct cubo HEXAHEDRO;
Así con estas definiciones una declaración de las siguientes
variables:

BYTE var1,var2;
HEXAEDRO var3;

Sería equivalente a:

unsigned char var1,var2;


struct cubo var3;

TEMA 4 : Punteros y funciones

4.0 Introducció
Introducción
n

En este tema estudiaremos el tipo de dato más importante


dentro del lenguaje C. Los punteros. Absolutamente
Absolutament e todos los datos
en C pueden ser tratados como punteros y por ello este lenguaje
proporciona una serie de importantes herramientas para trabajar con
ellos.

Además introduciremos
introducirem os el concepto de función asociado
estrechamente
estrechamente a la llamada programación
programación modular que nos permite
crear un programa
programa mucho más claro y fácil
fácil de corregir a la hora de
encontrar errores.

4.1 Punteros

4.1.1 ¿ Qué son los punteros ?

Como su nombre indica un puntero es algo que apunta, es


decir, nos indica dónde se encuentra una cierta cosa. Supongamos
(como otras tantas veces) que disponemos de un gran archivo en el
que almacenamos
a lmacenamos informes. Este fichero
fi chero está dividido
d ividido en
compartimientos,
compartimie ntos, cada uno de los cuales contiene uno de nuestros
informes (esto sería equivalente a las variables con las que hemos
trabajado hasta ahora -informes-, la cuales contienen información, y
el archivo representa la memoria de nuestro ordenador, obviamente
las variables
va riables se almacenan
alma cenan en
e n la memoria). Sin embargo
em bargo otros
compartimientos
compartimie ntos no contienen informes, sino que lo que contienen es
una nota que nos dice dónde está ese informe.

Supongamos que como máximo trabajamos con tres informes a la


vez, digamos que no nos gusta leer demasiado, y reservamos,
reservamos, por
tanto, tres compartimientos
compartimient os en los indicamos en que compartimiento
compartimien to
se encuentran
encuentran esos tres informes. Estos tres
tres compartimientos
compartimientos serían
serían
nuestros punteros y como ocupan un compartimiento
compartimient o en el archivo
(nuestra memoria) son realmente variables, pero variables muy
especiales. Estas variables punteros ocupan siempre un tamaño fijo,
simplemente contienen el número de compartimiento
compartimie nto en el que se
encuentra la información. No contienen la información en sí.

Si en nuestro archivo pudiésemos almacenar un máximo de


20.000 hojas, esta sería la capacidad de nuestra memoria (unos 19
Kilobytes). Estas hojas de nuestros informes las agruparíamos de
distintas formas. Quizá un informe sólo ocupe 5 páginas mientras que
otro puede ocupar 100. Podemos ver esto como los distintos tipos de
datos del C, es lógico pensar que necesitamos más espacio para
almacenar un número real que uno entero o que una matriz de 20x20
elemento. Estos son nuestro informes en nuestro archivo. Sin embargo
los punteros siempre ocupan lo mismo, en nuestro ejemplo nos
llegaría con una página para poder escribir el número del
compartimiento
compartimiento en el que se encuentra el inicio del informe.

Así en nuestro supuesto de que sólo trabajemos con tres


informes a la vez, dispondríamos
dispondríamos de tres compartimientos
compartimientos en los
los que
indicaríamos
indicaríamo s dónde se encuentran esos informes que buscamos y de
esta forma cuando terminemos con ellos y deseemos trabajar con otros
sólo tendremos que cambiar el contenido de esos tres compartimientos
diciendo donde se encuentran los nuevos informes. De esta forma no
es necesario reservar unos compartimientos
compartimie ntos para trabajar y cada vez
que cambiemos de trabajo llevar los informes viejos a su
compartimiento
compartimiento anterior
anterior y traer los nuevos
nuevos informes
informes a estos
compartimientos.

Esto es lo que en programación


programación se conoce como referencia
indirecta o indireción. Accedemos a la información a través de un
puntero que nos dice dónde se encuentra ésta. Y a grandes rasgos
ésto son los punteros, referencias indirectas a datos en la memoria
del ordenador.

Los punteros en C son muy importantes puesto que su


utilización es básica para la realización
realización de numerosas operaciones.
operaciones.
Entre ellas: paso de parámetros que deseamos sean modificados,
tratamiento de estructuras dinámicas de datos (ésto es, variables
que no se declaran en el programa y se crean durante la ejecución
del programa), cadenas de caracteres ...

4.1.2 Operadores que actúan sobre punteros.

El lenguaje C proporciona dos operadores relacionados


directamente con los punteros. El primero de ellos es el operador &.
Ya hemos visto este operador antes en las llamadas a la función
scanf, posteriormente explicaremos por que la función scanf necesita
ser llamada con el operador &.

El operador &, es un operador unario, es decir, actúa sobre


un sólo operando. Este operando tiene que ser obligatoriamente
obligatoriamente una
estructura direccionable,
direccionable , es decir, que se encuentre en la memoria
del ordenador. Estas estructuras son fundamentalmente
fundamentalme nte las variables
y las funciones, de las que hablaremos posteriormente.
posteriormente. Decimos que
sólo se puede aplicar sobre estructuras direccionables
direccionabl es porque su
función es devolver la posición de memoria en la que se encuentra
dicha estructura.
estructura. En nuestro ejemplo nos indicaría cual sería el
compartimiento en el que se encuentra el informe que le indiquemos.

El segundo operador es el *. También se trata de un operador


unario como el anterior y su función en este caso es la de permitir
el acceso al contenido de la posición indicada por un puntero. En
nuestro ejemplo el operador * nos permitiría leer o escribir el
informe al que apunta uno de nuestros compartimientos
compartimien tos punteros.
Además el carácter * se utiliza para declarar punteros los cuales
como ya dijimos tienen que ser declarados (tienen su propio
compartimiento
compartimiento en el archivo).

Por supuesto el operador * debe ser aplicado sobre un


puntero, mientras que el operador & sobre una estructura
direccionable
direccionable (variable
(variable o función). Veamos un
un ejemplo de su
utilización:
main ()
{
int x,y; /* Variables de tipo entero */
int *px; /* Puntero a una variable de tipo entero */

/* Leemos la dirección -compartimiento- de la variable


-informe- x mediante & y lo almacenamos en la variable puntero
px */
px = &x;
/* px contiene la dirección en la que se encuentra x */
/* Utilizando el operador *, podemos acceder a su
información. *px representa ahora el valor de la variable x */
*px = 10; /* Ahora x contiene el valor 10 */
y = 15;
/* Si ahora hacemos que nuestro puntero apunte a la
variable y utilizando de nuevo el operador & */
px = &y;
/* El valor que ahora toma *px será el valor de y puesto
que es el compartimie
compartimiento
nto al que ahora estamos apuntando */
*px = 125; /* Ahora y contiene el valor 125 */
x = *px /* Ahora x contiene también 125 */
}

Como hemos visto en este


es te ejemplo
eje mplo es exactamente
ex actamente igual
acceder a una variable que utilizar un puntero que apunte a ella
(hacemos que apunte a ella mediante el operador &) junto con el
operador *.

Pero el lenguaje C aún ofrece otra herramienta más para


trabajar con punteros. Es lo que se suele llamar aritmética de
punteros. Este tema lo trataremos en profundidad en el siguiente
apartado.

4.1.3 Punteros y matrices

Ya hemos hablado de las matrices en el tema anterior. Se


trataba de un
un conjunto de un número de terminado de variables de un
mismo tipo que se referenciaban
referenciaban con un nombre común seguido de su
posición entre corchetes con relación al primer elemento. Todas las
entradas de una matriz están consecutivas en memoria, por eso es muy
sencillo acceder al elemento que queramos en cada momento
simplemente indicando su posición. Sólo se le suma a la posición
inicial ese índice que indicamos. Es un ejemplo que casa
perfectamente
perfectamen te con nuestro ejemplo de los informes, cada informe
podría ser considerado como una matriz de tantos elementos como
páginas tenga el informe y enen los
los que
que cada
cada uno de ellos es un
un tipo
tipo
de datos llamado página.

Las matrices son realmente punteros al inicio de una zona


consecutiva de los elementos indicados en su declaración, por lo
cual podemos acceder a la matriz utilizando los corchetes como ya
vimos o utilizando el operador *.

elemento[i] <=> *(elemento +i)

Como ya se ha comentado todos los punteros ocupan lo mismo


en memoria, el espacio suficiente para contener una dirección, sin
embargo cuando se declaran es necesario indicar cual es el tipo de
datos al que van a apuntar (entero, real, alguna estructura definida
por el usuario). En nuestro ejemplo tendríamos un tipo de puntero
por cada tipo de informe distinto, un puntero para informes de una
página, otro puntero para informes de 2 páginas y así sucesivamente.
En principio esto es irrelevante por que una dirección de memoria es
una dirección
dirección de memoria, independiente
independientemente
mente de lo que
que contenga
contenga con
lo cual no sería necesario declarar ningún tipo, pero esta
información es necesaria
necesaria para implementar la aritmética
aritmética de punteros
que ejemplificaremos a continuación.
continuación.

Supongamos que hemos definido un tipo de datos en nuestro


programa que fuese página, si cada página puede contener 80
caracteres de ancho por 25 de alto, podría ser algo como ésto:

typedef char página[80][25];

Y supongamos también que sólo tenemos tres tipos de


informes, de 1 página, de 5 páginas y de 25 páginas:

typedef página informe1;


typedef página informe2[5];
typedef página informe3[25];
informe3[25] ;

Y en nuestro programa principal hemos declarado las


siguientes variables:

main()
{
página *punt_página;
informe1 i1[10],*punt1;
informe2 i3[5],*punt2;
informe3 i4[15],*punt3;

....

Por tanto disponemos de un puntero a páginas y tres


punteros, uno para cada tipo de informe y tres matrices de distintos
tipos de informes que nos permiten almacenar en nuestro archivo un
máximo de 30 informes (10 de 1 página, 5 de 5 páginas y 15 de 25
páginas).

Supongamos que en el programa principal se llenan esas


matrices con datos (por teclado o leyendo de un fichero, por
ejemplo) y realizamos las siguientes operaciones:

punt_página = (página *) &i4[0];


punt3 = (informe3 *)&i4[0];

Los cast (que comentamos en el tema 1) convierten las


direcciones al tipo apropiado, las direcciones que contendrán
punt_página y punt3 serán exactamente iguales, apuntarán al
principio del primer informe de tipo3. Sin embargo punt_página es un
puntero de tipo página y punt3 es un puntero de tipo informe3, ¨qué
significa ésto?. Si ejecutásemos una instrucción como ésta:

punt_página = punt_página + 5;

punt_página pasaría a apuntar a la quinta página del primer


informe de tipo 3 (i4[0]), puesto que punt_página es un puntero de
paginas. Mientras que si la operación fuese:

punt3 = punt3 + 5;
punt3 pasaría a apuntar a el quinto informe de tipo 3
(i4[5]), puesto que punt3 es un puntero a informes de tipo tres. Si
ahora realizásemos la operación:

punt_página = (página *)punt3;

Ahora punt página apuntaría a la primera página del quinto


informe de tipo 3. En esto consiste la aritmética de punteros,
cuando se realiza una operación aritmética sobre un puntero las
unidades de ésta son el tipo que se le ha asociado
asociado a dicho
dicho puntero.
puntero.
Si el puntero es de tipo página operamos con páginas, si es de tipo
informes operamos con informes. Es evidente que un informe de tipo 3
y una página tienen distintos tamaños (un informe de tipo 3 son 25
páginas por definición).
definición).

Como hemos visto las matrices se pueden considerar como


punteros y las operaciones con esos punteros depende del tipo
asociado al puntero, además es muy recomendable utilizar el cast
cuando se realizan conversiones de un tipo de puntero a otro.

4.1.4 Punteros y cadenas de caracteres

Como su propio nombre indica una cadena de caracteres es


precisamente
precisament e eso un conjunto consecutivo de caracteres. Como ya
habíamos comentado los caracteres se codifican utilizando el código
ASCII que asigna un número desde 0 hasta 255 a cada uno de los
símbolos representables
representabl es en nuestro ordenador. Las cadenas de
caracteres utilizan el valor 0 ('\0') para indicar su final. A este
tipo de codificación se le ha llamado alguna vez ASCIIZ (la Z es de
zero).

Las cadenas de caracteres se representan entre comillas


dobles (") y los caracteres simples, como ya habíamos indicado con
comillas simples ('). Puesto que son un conjunto consecutivo de
caracteres la forma de definirlas es como una matriz de caracteres.

char identificador[tamaño_de_la_cadena];

Y por ser en esencia una matriz todo lo comentado


anteriormente
anteriormente para matrices y punteros puede ser aplicado a ellas.
Así la siguiente definición constituye también una cadena de
caracteres:

char *identificador;

La diferencia entre ambas declaraciones


declaracione s es que la primera
reserva una zona de memoria de tamaño_de_la_cadena para almacenar el
mensaje que deseemos mientras que la segunda sólo genera un puntero.
La primer por tratarse de una matriz siempre tiene un puntero
asociado al inicio del bloque del tamaño especificado.
especificado . Podemos
tratar a las cadenas como punteros a caracteres (char *) pero
tenemos que
que recordar
recordar siempre que un puntero
puntero no contiene
contiene información
información
sólo nos indica dónde se encuentra ésta, por tanto con la segunda
definición no podríamos hacer gran cosa puesto que no tenemos
memoria reservada para ninguna información. Veamos un ejemplo para
comprender mejor la diferencia entra ambas declaraciones.
declaracione s.
Utilizaremos
Utilizaremo s dos funciones especiales de stdio.h para trabajar con
cadenas. Estas son puts y gets que definiríamos como un printf y un
scanf exclusivo para cadenas.
#include <stdio.h>
main()
{
char cadena1[10];
char cadena2[10];
char *cadena;

gets(cadena1);
gets(cadena1); /* Leemos un texto por teclado y lo
almacenamos en cadena 1 */
gets(cadena2);
gets(cadena2); /* Idem cadena2 */

puts (cadena1); /* Lo mostramos en pantalla */


puts (cadena2);

cadena = cadena1; /* cadena que sólo es un puntero ahora


apunta a cadena1 en donde tenemos 10 caracteres reservados por la
definición */

puts (cadena); /* Mostrara en pantalla el mensaje


contenido en cadena1 */
cadena = cadena2; /* Ahora cadena apunta a la segunda
matriz de caracteres */
gets(cadena);
gets(cadena) ; /* Cuando llenos sobre cadena ahora
estamos leyendo sobre cadena2, debido al efecto de la instrucción
anterior */
puts(cadena2);
puts(cadena2); /* SI imprimimos ahora cadena2 la pantalla
nos mostrará la cadena que acabamos de leer por teclado */
}

En el programa vemos como utilizamos cadena que solamente es


un puntero para apuntar a distintas zonas de memoria y utilizar
cadena1 o cadena2 como destino de nuestras operaciones. Como podemos
ver cuando cambiamos el valor de cadena a cadena1 o cadena2 no
utilizamos el operador de dirección &, puesto que como ya hemos
dicho una matriz es en sí un puntero (si sólo indicamos su nombre) y
por tanto una matriz o cadena de caracteres sigue siendo un puntero,
con lo cual los dos miembros de la igualdad son del mismo tipo y por
tanto no hay ningún problema.

4.2 Funciones

4.2.1 Introducció
Introducción
n

Hasta el momento hemos utilizado ya numerosas funciones,


como printf o scanf, las cuales forman parte de la librería estándar
de entrada/salida
entrada/salid a (stdio.h). Sin embargo el lenguaje C nos permite
definir nuestras propias funciones, es decir, podemos añadir al
lenguaje tantos comandos como deseemos.

Las funciones son básicas en el desarrollo de un programa


cuyo tamaño sea considerable,
considerabl e, puesto que en este tipo de programas
es común que se repitan fragmentos de código, los cuales se pueden
incluir en una función con el consiguiente ahorro de memoria. Por
otra parte el uso de funciones divide un programa de gran tamaño en
subprogramas
subprograma s más pequeños (las funciones), facilitando su
comprensión, así como la corrección de errores.

Cuando llamamos a una función desde nuestra función


principal main() o desde otra función lo que estamos haciendo
realmente es un salto o bifurcación al código que le hayamos
asignado, en cierto modo es una forma de modificar el flujo de
control del programa como lo hacíamos con los comandos while y for.

4.2.2 Definición de funciones

Ya hemos visto cual es la estructura general de una función


puesto que nuestro programa principal, main() no es otra cosa que
una función. Veamos cual es el esquema genérico:

tipo_a_devolver identificador (tipo1 parámetro1, tipo2 ...)


{
tipo1 Variable_Local1;
tipo2 Variable_Local2;
...

Código de la función

return valor del tipo valor a devolver;


}

Lo primero con lo que nos encontramos es la cabecera de la


función. Esta cabecera está formada por una serie de
declaraciones.
declaraciones. En primer lugar el tipo_a_devolver.
tipo_a_devolver.

Todas las funciones tienen la posibilidad de devolver un


valor, aunque pueden no hacerlo. Si definimos una función que nos
calcula el coseno de un cierto ángulo nos interesaría que nuestra
función devolviese ese valor. Si por el contrario nuestra función
realiza el proceso de borrar la pantalla no existiría ningún valor
que nos interesase conocer sobre
sobre esa función.
función. Si no se especifica
especifica
ningún parámetro el compilador supondrá que nuestra función
devuelve un valor entero (int).

A continuación
continuación nos encontramos
encontramos con el identificador
identificador de la
función, es
es decir, el nombre con el que la vamos a referenciar
referenciar en
nuestro programas, seguido de una lista de d e parámetros
parám etros entre
paréntesis y separados por comas sobre los que actuará el código
que escribamos para esa función. En el caso de la función coseno a
la que antes aludíamos, el parámetro sería el ángulo calculamos el
coseno de un cierto ángulo que en cada llamada a la función
probablemente
probablemente sea distinto. Véase la importancia de los
parámetros, si no pudiésemos definir un parámetro para nuestra
función coseno, tendríamos que definir una función para cada
ángulo, en la que obviamente no indicaríamos ningún parámetro.

A continuación nos encontramos el cuerpo de la función. En


primer lugar declaramos las variables locales de esa función.
Estas variables solamente podrán ser accedidas
acc edidas dentro de la
función, esto es, entre las llaves ({}). Los nombres de las
variables locales pueden ser los mismos en distintas funciones
puesto que sólo son accesibles dentro de ellas. Así si estamos
acostumbrados
acostumbrados a utilizar una variable entera llamada i como
contador en nuestro bucles, podemos definir en distintas funciones
esta variable y utilizarla dentro de cada función sin que haya
interferencias
interferencias entre las distintas funciones.

Con respecto al código de la función, pues simplemente se


trata de un programa como todos los que hemos estado haciendo
hasta ahora.
La instrucción return del final puede omitirse si la
función no devuelve ningún valor, su cometido es simplemente
indicar que valor tomaría esa función con los parámetros que le
hemos pasado. En otros lenguajes las funciones que no devuelven
valores se conocen como procedimientos.
procedimientos.

Veamos un ejemplo de definición de una función.

int busca_elemento
busca_elemento (int *vector,in
*vector,int
t valor,int longitud)
{
int i;

for (i=0;i<longitud;i++)
(i=0;i<longitud;i++)
if (vector[i] == valor) break;

return i;
}

Esta función busca un valor en un vector de números


enteros y devuelve el índice dentro de la matriz
matriz de la entrada
entrada de
ésta que lo contiene. Puesto que devuelve el índice de la matriz
supondremos en principio un valor de retorno entero para ese
índice. Los
Los parámetros
parámetros que debe conocer la
la función son: la matriz
en la que buscar, el valor que debemos buscar y la longitud de la
matriz. Podríamos haber realizado una función a medida para que
utilizase una matriz de un número determinado
determinado de elementos (int
vector[100],
vector[100] , por ejemplo) y ahorrar el parámetro longitud, sin
embargo con la definición que hemos hecho nuestra función
funcionará con matrices de cualquier longitud de enteros.

Hemos declarado además una variable local que es necesaria


para la realización del bucle actuando como contador y conteniendo
además el índice dentro de la matriz que buscamos. Si la entrada i
de nuestro vector corresponde con valor, salimos del bucle y la
variable i contiene ese valor de la entrada, el cual es devuelto
por la función
función mediante
mediante la instrucción return. Ahora
Ahora veremos
veremos como
utilizaríamos
utilizaríamos esta función desde un programa:

main ()
{
int matriz1[20];
int matriz2[30];
int indice,dato;

/* Aquí realizaríamos alguna operación sobre las


matrices como por ejemplo inicializarlas */

indice = busca_elemen
busca_elemento
to (matriz1,1
(matriz1,10,20);
0,20);
....
dato = 15;
indice = busca_elemento
busca_elemento (matriz2,d
(matriz2,dato,30);
ato,30);

.....
}

Como vemos en las llamadas a nuestra función podemos


utilizar tanto variables o constantes como parámetros.

4.2.3 Más sobre funciones


Cuando el valor que retornan las funciones no es entero,
es necesario que el compilador sepa de antemano su tipo por lo
cual es necesario añadir al comienzo del programa lo que se llaman
prototipos. Los prototipos simplemente son una predeclaración
predeclaraci ón de
la función, solo indican el tipo que devuelve, su nombre y los
tipos de los parámetros, no es necesario indicar un identificador
para los parámetros. Un prototipo para la función anterior sería:

int busca_elemento (int *, int, int);

Los fichero .h que se incluyen con la directiva del


procesador #include, contienen entre otras cosas los prototipos de
las funciones a las que nos dan acceso.

Para finalizar con las funciones vamos a explicar como


pasar parámetros que deseamos que la función modifique. Cuando
pasamos parámetros a una función ésta realiza una copia de los
valores de éstos en una zona de memoria propia, con lo cual la
función trabaja con estas copias de los valores y no hay peligro
de que se modifique la variable original con la que llamamos a la
función, forzando de esta forma a utilizar el valor retornado por
la función como parámetro. Sin embargo es posible que nos interese
que nuestra función nos devuelva más de una valor o que uno de los
parámetros con los que lo llamamos
llamamos se modifique
modifique en función de las
operaciones realizadas por la función. En este caso tenemos que
pasar los parámetros como punteros.

Cuando pasamos los valores como punteros la función


realiza una copia
copi a de los valores
va lores de los parámetros
par ámetros de las
funciones en su zona propia de memoria, pero en este caso el valor
que pasamos no es un valor en sí, sino que es una dirección de
memoria en la que se encuentra ese valor que deseamos se
modifique, es decir, creamos un puntero que apunta a la posición
que deseamos modificar, con lo cual tenemos acceso a esos valores.
Veamos un ejemplo típico de parámetros que deben modificarse, este
es la función swap(a,b) cuya misión es intercambiar los valores de
los dos parámetros,
parámetros, es decir, el parámetro a toma el valor del
parámetro b y viceversa. La primera codificación que se nos ocurre
sería ésta:

swap (int a,int b)


{
int t;

t = a;
a = b;
b = t;
}

Y nuestro programa principal podría ser algo como ésto:

main ()
{
int c,d;

c = 5;
d = 7;

swap (c,d);
}
Veamos que pasa en la memoria de nuestro ordenador.

-Función main()
-Espacio para la variable c (Posición de memoria x)
-Espacio para la variable d (Posición de memoria y)
-Inicialización
-Inicialización de las variables
-swap(c,d)
-Fin de main()
-Función swap
-Código de la función swap
-Espacio privado para almacenar los parámetros (Posición
de memoria z)

En este último compartimiento


compartimiento es dónde almacenamos los
valores de nuestros parámetros que serán respectivamente
respectivame nte 5 y 7.
Después de la ejecución de swap en esta zona de memoria los
valores están intercambiados,
intercambiados, nuestro parámetro a que se
corresponde con la variable c en la llamada a swap contendrá el
valor 7 y el parámetro b correspondiente
correspondi ente a d en la función main
contendrá el valor 5. Esto es lo que se encuentra almacenado en la
zona privada de memoria de la función. Con este esquema cuando la
función swap termina su ejecución y se devuelve el control al
programa principal main, los valores de c y d no han cambiado,
puesto que los compartimientos
compartimien tos o posiciones de memoria x e y no
han sido tocados por la función swap, la cual sólo ha actuado
sobre el compartimiento z.

Si declaramos ahora nuestra función swap como sigue:

swap (int *p1,int *p2)


{
int t;

t = *p1; /*Metemos en t el contenido de p1 */


*p1 = *p2;
*p2; /* Contenido
Contenido de p1
p1 = contenido
contenido de p2 */
*p2 = t;
}

Tendremos el mismo esquema de nuestra memoria que antes


pero en lugar de almacenar en la zona privada de la función swap
para los parámetros los valores 5 y 7 tenemos almacenados en ella
los compartimientos
compartimient os en los que se encuentran, ésto es, hemos
almacenado las posiciones x e y en lugar de 5 y 7. De esta forma
accedemos mediante un puntero a las variables c y d del programa
principal que se encuentran en las posiciones x e y modificándolas
modificándolas
directamente así que al regresar al programa principal sus valores
se encuentran ahora intercambiados.

En resumen, cuando deseemos que una función modifique el


valor de uno de los parámetros con los que es llamada debemos
pasar un puntero a esa variable en lugar del valor de esa
variable. Es evidente que si implementamos nuestra función de esta
forma, los parámetros jamás podrán ser constantes, puesto que
difícilmente podríamos modificar el valor de una constante.

Vous aimerez peut-être aussi