0% encontró este documento útil (0 votos)
75 vistas

Manual C

manual C++
Derechos de autor
© © All Rights Reserved
Formatos disponibles
Descarga como PDF, TXT o lee en línea desde Scribd
0% encontró este documento útil (0 votos)
75 vistas

Manual C

manual C++
Derechos de autor
© © All Rights Reserved
Formatos disponibles
Descarga como PDF, TXT o lee en línea desde Scribd
Está en la página 1/ 397

UNIVERSIDAD

POLITÉCNICA
DE CARTAGENA

Área de Lenguajes y Sistemas Informáticos

FUNDAMENTOS DE INFORMÁTICA.
PROGRAMACIÓN EN C.
Pedro María Alcover Garau

Ingeniero Industrial
Ingeniero Técnico Industrial
FUNDAMENTOS DE INFORMÁTICA.
PROGRAMACIÓN EN C.
Pedro María Alcover Garau

Área de Lenguajes y Sistemas Informáticos


Universidad Politécnica de Cartagena

Septiembre, 2005
© Pedro María Alcover Garau

Edita Universidad Politécnica de Cartagena


Segunda Edición revisada y ampliada: Junio 2007.

ISBN: 84–95781–61–1

Depósito Legal MU–1869–2005.

Imprime Morpi, S.L.


A todos los alumnos
de la Universidad Politécnica de Cartagena
que estudian.
A muchos les he tenido en mis clases.
Doy gracias a Dios por haberles conocido.
PRESENTACIÓN
Después de varios años de dar clases de fundamentos de informática y
de programación en lenguaje C en algunas titulaciones de la Escuela de
Ingenieros Industriales de la UPCT, me he decidido a poner por escrito,
para guía del alumno, un manual que recoja toda la materia que se
imparte en esas asignaturas.

La información que aquí se recoge supera las posibilidades de docencia


que pueden alcanzarse en una asignatura cuatrimestral. He querido
extenderme más allá de lo que es posible ver en las aulas, porque creo
que vale la pena ofrecer la posibilidad de que alguien que adquiera este
manual para apoyo de una asignatura, pueda luego continuar
estudiando y ampliar sus conocimientos, y profundizar.

De nuevo he tenido el tiempo muy limitado para la revisión y ampliación


de estas páginas. Ahora es urgente mandar estas páginas a imprenta,
porque el curso académico comienza hoy mismo.

Seguro que quien haga uso de este manual detectará errores y erratas.
También apreciará modos de decir que podrían mejorarse, y que
ofrecerían una mayor claridad de exposición.
Agradeceré recibir todas las sugerencias, porque así se podrá ofrecer, a
quienes vengan detrás, una versión del manual mejorada. Se puede
contactar conmigo a través del correo electrónico. Mi dirección es
[email protected].

Muchas gracias.

Cartagena, 25 de septiembre de 2006

Con fecha 20 de junio de 2007 termino una revisión en la que he


incluido todas las modificaciones que los alumnos y profesores me han
sugerido. Es evidente que agradezco esta colaboración; algunos errores
dificultaban la comprensión del texto.

A partir de algunas dudas que me han planteado los alumnos he


decidido introducir algunos nuevos epígrafes que no estaban en la
última versión del manual. No añade nada sustancial al texto anterior.
ÍNDICE

PARTE I. Primeros pasos en C 1

CAPÍTULO 1
LENGUAJE C. 5
Introducción 5
Entorno de programación. 7
Estructura básica de un programa en C. 9
Elementos léxicos 12
Sentencias simples y sentencias compuestas 13
Errores y depuración 14
Recapitulación 15
El entorno de Borland C++ 15

CAPÍTULO 2
TIPOS DE DATOS Y VARIABLES EN C 19
Declaración de variables. 20
Tipos de datos primitivos en C: sus dominios. 22
Tipos de datos primitivos en C: sus operadores. 25
Operador asignación. 26
Operadores aritméticos. 27
Operadores relacionales y lógicos. 30
Operadores a nivel de bit. 34

i
Operadores compuestos. 39
Operador sizeof 40
Expresiones en las que intervienen variables de diferente
tipo. 41
Operador para forzar cambio de tipo. 43
Propiedades de los operadores. 45
Valores fuera de rango en una variable. 48
Constantes. Directiva #define. 50
Intercambio de valores de dos variables. 51
Ayudas On line. 52
Recapitulación 52
Ejemplos y Ejercicios propuestos (del 1 al 15) 53

CAPÍTULO 3
FUNCIONES DE ENTRADA Y SALIDA POR
CONSOLA 65
Salida de datos. La función printf() 66
Entrada de datos. La función scanf() 74
Recapitulación 76
Ejercicios (del 16 al 19) 77
ANEXO: Ficha resumen de la función printf() 83

CAPÍTULO 4
ESTRUCTURAS DE CONTROL 87
Introducción 87
Conceptos previos 89
Estructuras de control condicionales 90
Estructura de selección múltiple: Sentencia switch 97
Un ejercicio planteado. 101
Estructuras de repetición. Iteración. 104
Sentencias de salto: break y continue 116
Palabra reservada goto 120
Variables de control de las iteraciones 120
Recapitulación. 122
Ejercicios. (del 20 al 39) 122

ii
CAPÍTULO 5
ÁMBITO Y VIDA DE LAS VARIABLES 147
Ámbito y Vida. 147
El almacenamiento de las variables en la memoria. 148
Variables Locales y Variables Globales 150
Variables estáticas y dinámicas. 154
Variables en registro. 156
Variables extern 157
En resumen… 158
Ejercicios (40) 160

CAPÍTULO 6
ARRAYS NUMÉRICOS: VECTORES Y MATRICES 163
Noción y declaración de array. 164
Noción y declaración de array de dimensión múltiple, o
matrices. 166
Ejercicios (del 41 al 47) 169

CAPÍTULO 7
CARACTERES Y CADENAS DE CARACTERES 181
Operaciones con caracteres. 182
Entrada de caracteres. 185
Cadena de caracteres. 186
Dar valor a una cadena de caracteres. 188
Operaciones con cadenas de caracteres. 191
Otras funciones de cadena. 196
Ejercicios. (del 48 al 51) 199

CAPÍTULO 8
PUNTEROS 203
Definición y declaración 204
Dominio y operadores para los punteros 205
Punteros y vectores 209
Índices y operatoria de punteros 212

iii
Puntero a puntero 215
Advertencia final 219
Ejercicios. (del 52 al 53) 221

CAPÍTULO 9
FUNCIONES 223
Definiciones 224
Funciones en C 227
Declaración de la función. 228
Definición de la función. 230
Llamada a la función 232
La sentencia return 233
Ámbito y vida de las variables 236
Recurrencia 239
Llamadas por valor y llamadas por referencia 243
Vectores y matrices como argumentos 246
Funciones de escape 249
Ejercicios. (del 54 al 63) 250

PARTE II. Profundizando en C 273

CAPÍTULO 10
ASIGNACIÓN DINÁMICA DE MEMORIA 277
Función malloc 279
Función free 283
Ejemplo: la Criba de Erastóthenes. (Ejercicio 64) 283
Matrices en memoria dinámica 288
Ejercicios. (65) 292

CAPÍTULO 11
ALGUNOS USOS CON FUNCIONES 299
Punteros a funciones 300
Vectores de punteros a funciones 303

iv
Funciones como argumentos 305
Ejemplo: la función qsort 308
Estudio de tiempos 312
Creación de MACROS 315
Ejemplo de MACRO: la Criba de Erastóthenes 316
Funciones con un número variable de argumentos 320
Argumentos de la línea de órdenes 325
Ejercicios. (del 66 al 67) 328

CAPÍTULO 12
ESTRUCTURAS ESTÁTICAS DE DATOS Y
DEFINICIÓN DE TIPOS 329
Tipos de dato enumerados 330
Dar nombre a los tipos de dato 331
Estructuras de datos y tipos de dato estructurados 333
Estructuras de datos en C 334
Vectores y punteros a estructuras 339
Anidamiento de estructuras 341
Tipo de dato union 344
Ejercicios. (68) 348

CAPÍTULO 13
GESTIÓN DE ARCHIVOS 353
Tipos de dato con persistencia 354
Archivos y sus operaciones 356
Archivos de texto y binarios 359
Tratamiento de archivos en el lenguaje C 359
Archivos secuenciales con buffer. 361
Entrada y salida sobre archivos de acceso aleatorio 373
Ejercicios. (del 69 al 70) 375

v
vi
PARTE I:
Primeros pasos
en lenguaje C.
Fundamentos de informática. Programación en Lenguaje C

2
PARTE I: Primeros pasos en lenguaje C.

El lenguaje C es un lenguaje de alto o medio nivel.

Un lenguaje de programación es un conjunto de palabras, de


símbolos y de reglas para combinar estos símbolos, que se usan para
expresar algoritmos y construir programas. Los lenguajes de
programación, como todos los lenguajes, poseen un léxico (vocabulario
o conjunto de símbolos permitidos), una sintaxis (que recoge las reglas
que indican cómo realizar construcciones de lenguaje) y una semántica
(que indica el significado de cada construcción concreta).

A lo largo de esta primera parte del manual, veremos cómo programar


en C. Aprenderemos muchas de las palabras el léxico de C y las normas
sintácticas para construir sentencias y programas. Comenzaremos con
un breve capítulo introductorio donde se presentan los conceptos
básicos de los lenguajes de programación y, en concreto, del lenguaje C.
Y capítulo a capítulo iremos aprendiendo a trabajar y programar en este
lenguaje. Mostraremos el modo en que se crean las variables que
almacenarán la información de nuestros programas y diferentes
características y propiedades de las variables creadas (Capítulos 2 y 5);
La forma que tiene el lenguaje C de lograr comunicación entre el
programa y el usuario: mostrar datos por pantalla o solicitar nueva
información por teclado (Capítulo 3). El capítulo 4 está enteramente
dedicado a las estructuras de control, que permiten modificar el orden
secuencial de ejecución de instrucciones, o decidir la ejecución de una
instrucción u otra alternativa: veremos cómo se expresan estas
estructuras en el lenguaje C. También veremos cómo crear estructuras
sencillas de datos: agrupaciones de variables del mismo tipo, creadas

3
Fundamentos de informática. Programación en Lenguaje C

como cadenas de caracteres o como vectores o matrices numéricas


(Capítulos 6 y 7)

Al final de esta Primera Parte tenemos un primer capitulo introductorio


para las funciones (Capítulo 9); le seguirá luego otro de ampliación de
nociones, incluido ya en la Tercera Parte del manual. Pero antes de
llegar a este último capítulo de esta Primera Parte, veremos, en el
Capítulo 8, un aspecto de la programación en C, ciertamente
controvertido, que aporta cierta complejidad y peligro a la programación
en el lenguaje C, pero que ofrece también, a cambio, cierta capacidad
de dominio del programador sobre la gestión de memoria del programa:
los punteros.

Esta primera parte completa la información que el alumno de primero de


una carrera de industriales debe aprender. En la Segunda Parte
quedarán recogidos aquellos Capítulos para un posterior estudio, por si
algún alumno desea o requiere saber más sobre la programación en C.

Esta manual es continuación de otro de estudio previo titulado


“Fundamentos de Informática. Codificación y Algoritmia”, editado,
como éste, por la Universidad Politécnica de Cartagena, y del mismo
autor que el presente manual.

4
CAPÍTULO 1
LENGUAJE C.

Presentamos en este capítulo una primera vista de la programación en


lenguaje C. El objetivo ahora es mostrar los conceptos básicos de un
entorno de programación, y redactar, con el entorno que cada uno
quiera (a lo largo del curso emplearemos fundamentalmente el Turbo
C++, de la casa Borland), un primer programa en C, que nos servirá
para conocer las partes principales de un programa.

Introducción.
Los lenguajes de programación están especialmente diseñados para
programar computadoras. Sus características fundamentales son:

1. Son independientes de la arquitectura física del ordenador.


Los lenguajes están, además, normalizados, de forma que queda
garantizada la portabilidad de los programas escritos en esos
lenguajes.
Fundamentos de informática. Programación en Lenguaje C.

2. Normalmente un mandato en un lenguaje de alto nivel da lugar, al


ser introducido, a varias instrucciones en lenguaje máquina.

3. Utilizan notaciones cercanas a las habituales, con sentencias y


frases semejantes al lenguaje matemático o al lenguaje natural.

El lenguaje C se diseñó en 1969. El lenguaje, su sintaxis y su semántica,


así como el primer compilador de C fueron diseñados y creados por
Dennis M. Ritchie y Ken Thompson, en los laboratorios Bell. Más tarde,
en 1983, se definió el estándar ANSI C (que es el que aquí
presentaremos).

El lenguaje C tiene muy pocas reglas sintácticas, sencillas de aprender.


Su léxico es muy reducido: tan solo 32 palabras.

A menudo se le llama lenguaje de medio nivel, más próximo al código


máquina que muchos lenguajes de más alto nivel. Es un lenguaje
apreciado en la comunidad científica por su probada eficiencia. Es el
lenguaje de programación más popular para crear software de sistemas,
aunque también se utiliza para implementar aplicaciones. Permite el uso
del lenguaje ensamblador en partes del código, trabaja a nivel de bit, y
permite modificar los datos con operadores que manipulan bit a bit la
información. También se puede acceder a las diferentes posiciones de
memoria conociendo su dirección.

El lenguaje C es un lenguaje del paradigma imperativo, estructurado.


Permite con facilidad la programación modular, creando unidades que
pueden compilarse de forma independiente, que pueden posteriormente
enlazarse. Así, se crean funciones o procedimientos, que se pueden
compilar y almacenar, creando bibliotecas de código ya editado y
compilado que resuelve distintas operaciones. Cada programador puede
diseñar sus propias bibliotecas, que simplifican luego considerablemente
el trabajo futuro. El ANSI C posee una amplia colección de bibliotecas de
funciones estándar y normalizadas.

6
Capítulo 1. Lenguaje C.

Entorno de programación.
Para realizar la tarea de escribir el código de una aplicación en un
determinado lenguaje, y poder luego compilar y obtener un programa
que realiza la tarea planteada, se dispone de lo que se denomina un
entorno de programación.

Un entorno de programación es un conjunto de programas necesarios


para construir, a su vez, otros programas. Un entorno de programación
incluye editores, compiladores, archivos para incluir, archivos de
biblioteca, enlazadores y depuradores (ya veremos todos estos
conceptos en el primer Capítulo de este manual). Gracias a Dios existen
entornos de programación integrados, de forma que en una sola
aplicación quedan reunidos todos estos programas. Ejemplos de
entornos integrados de programación en C son el programa Microsoft
Visual C++, o el Turbo C++ de Borland.

Un editor es un programa que permite construir ficheros de caracteres,


que el programador introduce a través del teclado. Un programa no es
más que archivo de texto. El programa editado en el lenguaje de
programación se llama fichero fuente. Algunos de los editores facilitan
el correcto empleo de un determinado lenguaje de programación, y
advierten de inmediato la inserción de una palabra clave, o de la
presencia de un error sintáctico, marcando el texto de distintas formas.

Un compilador es un programa que compila, es decir, genera ficheros


objeto que “entiende” el ordenador. Un archivo objeto todavía no es
una archivo ejecutable.

El entorno ofrece también al programador un conjunto de archivos para


incluir o archivos de cabecera. Esos archivos suelen incluir
abundantes parámetros que hacen referencia a diferentes características
de la máquina sobre la que se está trabajando. Así, el mismo programa
en lenguaje de alto nivel, compilado en máquinas diferentes, logra
archivos ejecutables distintos. Es decir, el mismo código fuente es así

7
Fundamentos de informática. Programación en Lenguaje C.

portable y válido para máquinas diferentes.

Otros archivos son los archivos de biblioteca. Son programas


previamente compilados que realizan funciones específicas. Suele
suceder que muy diversos programas tienen idénticas o muy parecidas
muchas partes del código. Ciertas partes que son ya conocidas porque
son comunes a la mayor parte de los programas están ya escritas y
vienen recogidas y agrupadas en archivos que llamamos bibliotecas.
Ejemplos de estas funciones son muchas matemáticas (trigonométricas,
o numéricas,…) o funciones de entrada de datos desde teclado o de
salida de la información del programa por pantalla. Desde luego, para
hacer uso de una función predefinida, es necesario conocer su existencia
y tener localizada la biblioteca donde está precompilada; eso es parte
del aprendizaje de un lenguaje de programación, aunque también se
disponen de grandes índices de funciones, de fácil acceso para su
consulta.

Al compilar un programa generamos un archivo objeto. Habitualmente


los programas que compilemos harán uso de algunas funciones de
biblioteca; en ese caso, el archivo objeto no es aún un fichero
ejecutable, puesto que le falta añadir el código de esas funciones. Un
entorno de programación que tenga definidas bibliotecas necesitará
también un enlazador que realice la tarea de “juntar” el archivo objeto
con las bibliotecas empleadas y llegar, así, al código ejecutable.

La creación e implementación de un programa no suele terminar con


este último paso descrito. Con frecuencia se encontrarán errores, bien
de compilación porque haya algún error sintáctico o de expresión y
manejo del lenguaje; bien de ejecución, porque el programa no haga
exactamente lo que se deseaba. No siempre es sencillo encontrar los
errores de nuestros programas; un buen entorno de programación
ofrece al programador algunas herramientas llamadas depuradores,
que facilitan esta tarea.

Podríamos escribir el algoritmo que define la tarea de crear un

8
Capítulo 1. Lenguaje C.

programa. Ese algoritmo podría tener el aspecto del recogido en el


flujograma de la Figura 1.1.

Escritura del programa Compilación


I
fuente (.cpp)

Sí Errores de No Obtención del


compilación programa objeto (.obj)

Obtención del
programa Enlace
ejecutable (.exe)

Sí Errores de No
ejecución
Programas Archivos de
objeto del biblioteca (.lib)
Figura 1.1.: Fases de F
usuario
desarrollo de un programa

En el caso del lenguaje C, el archivo de texto donde se almacena el


código tendrá un nombre (el que se quiera) y la extensión .cpp (si
trabajamos con un entorno de programación de C++), o .c. Al compilar
el fichero fuente (nombre.cpp) se llega al código máquina, con el mismo
nombre que el archivo donde está el código fuente, y con la extensión
.obj. Casi con toda probabilidad en código fuente hará uso de funciones
que están ya definidas y precompiladas en las bibliotecas. Ese código
precompilado está en archivos con la extensión .lib. Con el archivo .obj
y los necesarios .lib que se deseen emplear, se procede al linkado o
enlazado que genera un fichero ejecutable con la extensión .exe.

Estructura básica de un programa en C.


Aquí viene escrito un sencillo programa en C. Quizá convenga ponerse
ahora delante del ordenador y, con el editor de C en la pantalla, escribir
estar líneas y ejecutarlas. Más adelante se muestra cómo trabajar en un
entorno de programación (se toma el Borland C++).

9
Fundamentos de informática. Programación en Lenguaje C.

#include <stdio.h>
/* Este es un programa en C. */
// Imprime un mensaje en la pantalla del ordenador
void main(void)
{
printf(“mi primer programa en C”);
}

Todos los programas en C deben tener ciertos componentes fijos. Vamos


a ver los que se han empleado en este primer programa:

1. #include <stdio.h>: Los archivos .h son los archivos de cabecera


en C. Con esta línea de código se indica al compilador que se desea
emplear, en el programa redactado, alguna función que está
declarada en el archivo de biblioteca stdio.h. Esta archivo contiene
las declaraciones de una colección de programas de entrada y salida
por consola (pantalla y teclado).

Esta instrucción nos permite utilizar cualquiera de las funciones


declaradas en el archivo. Esta línea de código recoge el nombre del
archivo stdio.h, donde están recogidos todos los prototipos de las
funciones de entrada y salida estándar. Todo archivo de cabecera
contiene identificadores, constantes, variables globales, macros,
prototipos de funciones, etc.

Toda línea que comience por # se llama directiva de


preprocesador. A lo largo del libro se irán viendo diferentes
directivas.

2. main: Es el nombre de una función. Es la función principal y


establece el punto donde comienza la ejecución del programa. La
función main es necesaria en cualquier programa de C que desee
ejecutar instrucciones. Un código será ejecutable si y sólo si
dispone de la función main.

3. void main(void): Los paréntesis se encuentran siempre después de


un identificador de función. Entre ellos se recogen los parámetros

10
Capítulo 1. Lenguaje C.

que se pasan a la función al ser llamada. En este caso, no se recoge


ningún parámetro, y entre paréntesis se indica el tipo void. Ya se
verá más adelante qué significa esta palabra. Delante del nombre de
la función principal (main) también viene la palabra void, porque la
función principal que hemos implementado no devuelve ningún
valor.

4. /* comentarios */: Símbolos opcionales. Todo lo que se encuentre


entre estos dos símbolos son comentarios al programa fuente y no
serán leídos por el compilador.

Los comentarios no se compilan, y por tanto no son parte del


programa; pero son muy necesarios para lograr unos códigos
inteligibles, fácilmente interpretables tiempo después de que hayan
sido redactados y compilados. Es muy conveniente, cuando se
realizan tareas de programación, insertar comentarios con frecuencia
que vayan explicando el proceso que se está llevando en cada
momento. Un programa bien documentado es un programa que
luego se podrá entender con facilidad y será, por tanto, más
fácilmente modificado y mejorado.

También se pueden incluir comentarios precediéndolos de la doble


barra //. En ese caso, el compilador no toma en consideración lo
que esté escrito desde la doble barra hasta el final de la presente
línea.

5. ;: Toda sentencia en C termina con el punto y coma. En C, se


entiende por sentencia todo lo que tenga, al final, un punto y coma.
La línea antes comentada (#include <stdio.h>) no termina con un
punto y coma porque no es una sentencia: es (ya lo hemos dicho)
una directiva de preprocesador.

6. {}: Indican el principio y el final de todo bloque de programa.


Cualquier conjunto de sentencias que se deseen agrupar, para
formar entre ellas una sentencia compuesta o bloque, irán marcadas

11
Fundamentos de informática. Programación en Lenguaje C.

por un par de llaves: una antes de la primera sentencia a agrupar; la


otra, de cierre, después de la última sentencia. Una función es un
bloque de programa y debe, por tanto, llevarlas a su inicio y a su fin.

Elementos léxicos.
Entendemos por elemento léxico cualquier palabra válida en el
lenguaje C. Serán elementos léxicos, o palabras válidas, todas aquellas
palabras que formen parte de las palabras reservadas del lenguaje, y
todas aquellas palabras que necesitemos generar para la redacción del
programa, de acuerdo con una normativa sencilla.

Para crear un identificador (un identificador es un símbolo empleado


para representar un objeto dentro de un programa) en el lenguaje C se
usa cualquier secuencia de una o más letras (de la ‘A’ a la ‘Z’, y de la
‘a’ a la ‘z’, excluida las letras ‘Ñ’ y ‘ñ’), dígitos (del ‘0’ al ‘9’) o símbolo
subrayado (‘_’). Un identificador es cualquier palabra válida en C. Con
ellos podemos dar nombre a variables, constantes, tipos de dato,
nombres de funciones o procedimientos, etc. También las palabras
propias del lenguaje C son identificadores; estas palabras se llaman
palabras clave o palabras reservadas.

Además de la restricción en el uso de caracteres válidos para crear


identificadores, existen otras reglas básicas para su creación en el
lenguaje C:

1. Debe comenzar por una letra del alfabeto o por el carácter


subrayado. Un identificador no puede comenzar por un dígito.

2. El compilador sólo reconoce los primeros 32 caracteres de un


identificador, pero éste puede tener cualquier otro tamaño mayor.
Aunque no es nada habitual generar identificadores tan largos, si
alguna vez así se hace hay que evitar que dos de ellos tengan
iguales los 32 primeros caracteres, porque entonces para el
compilador ambos identificadores serán el mismo.

12
Capítulo 1. Lenguaje C.

3. Las letras de los identificadores pueden ser mayúsculas y


minúsculas. El compilador distingue entre unas y otras, y dos
identificadores que se lean igual y que se diferencien únicamente en
que una de sus letras es mayúscula en uno y minúscula en otro, son
distintos.

4. Un identificador no puede deletrearse igual y tener el mismo tipo de


letra (mayúscula o minúscula) que una palabra reservada o que una
función definida en una librería que se haya incluido en el programa
mediante una sentencia include.

Las palabras reservadas, o palabras clave, son identificadores


predefinidos que tienen un significado especial para el compilador de C.
Sólo se pueden usar en la forma en que han sido definidos. El conjunto
de palabras clave o reservadas (que siempre van en minúscula) en ANSI
C es muy reducido (un total de 32) y son las siguientes:

auto double int struct


break else long switch
case enum register typedef
char extern return union
const float short unsigned
continue for signed void
default (goto) sizeof volatile
do if static while

A lo largo del manual se verá el significado de cada una de ellas. La


palabra goto viene recogida entre paréntesis porque, aunque es una
palabra reservada en C y su uso es sintácticamente correcto, de hecho
no es una palabra permitida en un paradigma de programación
estructurado como es el paradigma del lenguaje C.

Sentencias simples y sentencias compuestas.


Una sentencia simple es cualquier expresión válida en la sintaxis de C
que termine con el carácter de punto y coma. Sentencia compuesta es

13
Fundamentos de informática. Programación en Lenguaje C.

una sentencia formada por una o varias sentencias simples.

La sentencia simple queda definida cuando el programador termina una


expresión válida en C, con un punto y coma. La sentencia compuesta se
inicia con una llave de apertura ({) y se termina con una llave de
clausura (}).

Errores y depuración.
No es extraño que, al terminar de redactar el código de un programa, al
iniciar la compilación, el compilador deba abortar su proceso y avisar de
que existen errores. El compilador ofrece algunos mensajes que
clarifican frecuentemente el motivo del error, y la corrección de esos
errores no comporta habitualmente demasiada dificultad. A esos errores
sintácticos los llamamos errores de compilación. Ejemplo de estos
errores pueden ser que se haya olvidado terminar una sentencia con el
punto y coma, o que falte una llave de cierre de bloque de sentencias
compuestas, o sobre un paréntesis, o se emplee un identificador mal
construido…

Otras veces, el compilador no haya error sintáctico alguno, y compila


correctamente el programa, pero luego, en la ejecución, se producen
errores que acaban por abortar el proceso. A esos errores los llamamos
errores de ejecución. Un clásico ejemplo de este tipo de errores es
forzar al ordenador a realizar una división por cero, o acceder a un
espacio de memoria para el que no estamos autorizados. Esos errores
también suelen ser sencillos de encontrar, aunque a veces, como no son
debidos a fallos sintácticos ni de codificación del programa sino que
pueden estar ocasionados por el valor que en un momento concreto
adquiera una variable, no siempre son fácilmente identificables, y en
esos casos puede ser necesario utilizar los depuradores que muchos
entornos de programación ofrecen.

Y puede ocurrir también que el código no tenga errores sintácticos, y por

14
Capítulo 1. Lenguaje C.

tanto el compilador termine su tarea y genere un ejecutable; que el


programa se ejecute sin contratiempo alguno, porque en ningún caso se
llega a un error de ejecución; pero que el resultado final no sea el
esperado. Todo está sintácticamente bien escrito, sin errores de
compilación ni de ejecución, pero hay errores en el algoritmo que
pretende resolver el problema que nos ocupa. Esos errores pueden ser
ocasionados sencillamente por una errata a la hora de escribir el código,
que no genera un error sintáctico, ni aborta la ejecución: por ejemplo,
teclear indebidamente el operador suma (+) cuando el que correspondía
era el operador resta (-).

Todo error requiere una modificación del programa, y una nueva


compilación. No todos los errores aparecen de inmediato, y no es
extraño que surjan después de muchas ejecuciones.

Recapitulación.
En este capítulo hemos introducido los conceptos básicos iniciales para
poder comenzar a trabajar en la programación con el lenguaje C. Hemos
presentado el entorno habitual de programación y hemos visto un
primer programa en C (sencillo, desde luego) que nos ha permitido
mostrar las partes básicas del código de un programa: las directivas de
preprocesador, los comentarios, la función principal, las sentencias
(simples o compuestas) y las llaves que agrupan sentencias. Y hemos
aprendido las reglas básicas de creación de identificadores.

El entorno de Borland C++.


Al ejecutar la aplicación de Borland C++, aparece una pantalla gris. Lo
primero que se debe hacer es crear un nuevo documento donde
podremos escribir nuestro código. Para esto basta seguir los pasos
indicados en la Figura 1.2.

15
Fundamentos de informática. Programación en Lenguaje C.

Figura 1.2.: Borland C++. Creación de nuevo documento.

Una vez creado el documento, ya podemos escribir en él. Vamos a


copiar el texto del programa ejemplo de este capítulo. En la figura 1.3.
vemos el código copiado. Podemos ver cómo ha quedado escrito en
letras azules todo lo que es comentario. En letras verdes las directivas
de preprocesador. Y en letras también azules los textos recogidos entre
comillas. Todo este código de colores (y otros colores de letra que se
irán viendo), y de tipos de letra (cursiva, negrita) ayudan en la
confección del programa.

Supongamos ahora que guardamos el archivo en una carpeta nueva


llamada “PrimerPrograma”. El archivo (con extensión cpp) ocupa 167
bytes aunque por exigencias del disco donde se almacena el archivo
emplea hasta un total de 4 Kbytes.

Al compilar el programa, aparecen en la carpeta otros tantos archivos:


uno con la extensión obj, y otro con la extensión exe, que es el
ejecutable. Los tamaños de ambos archivos pueden verse buscando las
propiedades de ambos.

16
Capítulo 1. Lenguaje C.

Para compilar y ejecutar basta con elegir la opción Menú Debug Æ Run
o, como indica esa misma opción, pulsar simultáneamente las teclas
Control y F9. Una tercera opción es pulsar el botón en forma de rayo
amarillo situado en la sexta posición de la barra de botones.

Figura 1.3.: Borland C++. Nuestro primer programa.

17
Fundamentos de informática. Programación en Lenguaje C.

18
CAPÍTULO 2
TIPOS DE DATOS Y VARIABLES EN C

Un tipo de dato define de forma explícita un conjunto de valores,


denominado dominio, sobre el cual se pueden realizar una serie de
operaciones. Un valor es un elemento del conjunto que hemos llamado
dominio. Una variable es un espacio de la memoria destinada al
almacenamiento de un valor de un tipo de dato concreto, referenciada
por un nombre. Son conceptos sencillos, pero muy necesarios para
saber exactamente qué se hace cuando se crea una variable en un
programa.

Un tipo de dato puede ser tan complejo como se quiera. Puede necesitar
un byte para almacenar cualquier valor de su dominio, o requerir de
muchos bytes.

Cada lenguaje ofrece una colección de tipos de datos, que hemos


llamado primitivos. También ofrece herramientas para crear tipos de
dato distintos, más complejos que los primitivos y más acordes con el
tipo de problema que se aborde en cada momento.
Fundamentos de informática. Programación en Lenguaje C

En este capítulo vamos a presentar los diferentes tipos de datos


primitivos que ofrece el lenguaje C. Veremos cómo se crean (declaran)
las variables, qué operaciones se pueden realizar con cada una de ellas,
y de qué manera se pueden relacionar unas variables con otras para
formar expresiones. Veremos las limitaciones en el uso de las variables
según su tipo de dato.

Ya hemos dicho que un tipo de dato especifica un dominio sobre el que


una variable de ese tipo puede tomar sus valores; y unos operadores. A
lo largo del capítulo iremos presentando los distintos operadores básicos
asociados con los tipos de dato primitivos del lenguaje C. Es importante
entender la operación que realiza cada operador y sobre qué dominio
este operador está definido.

Declaración de variables.
Antes de ver los tipos de dato primitivos, conviene saber cómo se crea
una variable en C.

Toda variable debe ser declarada previa a su uso. Declarar una


variable es indicar al programa un identificador o nombre para esa
variable, y el tipo de dato para la que se crea esa variable.

La declaración de variable tiene la siguiente sintaxis:

tipo var_1 [=valor1, var_2 = valor_2, …, var_N = valor_N];

Donde tipo es el nombre del tipo de variable que se desea crear, y


var_1, es el nombre o identificador de esa variable.

Aclaración a la notación: en las reglas sintácticas de un lenguaje de


programación, es habitual colocar entre corchetes ([]) aquellas partes
de la sintaxis que son optativas.

En este caso tenemos que en una declaración de variables se pueden


declarar una o más variables del mismo tipo, todas ellas separadas por
el operador coma. Al final de la sentencia de declaración de variables

20
Capítulo 2. Tipos de datos y variables en C.

se encuentra, como siempre será en cualquier sentencia, el operador


punto y coma.

En la declaración de una variable, es posible asignarle un valor de inicio.


De lo contrario, la variable creada adquirirá un valor cualquiera entre
todos los explicitados por el rango del tipo de dato, desconocido para el
programador.

¿Qué ocurre si una variable no es inicializada? En ese caso, al declararla


se dará orden de reservar una cantidad de memoria (la que exija el tipo
de dato indicado para la variable) para el almacenamiento de los valores
que pueda ir tomando esa variable creada. Esa porción de memoria es
un elemento físico y, como tal, deberá tener un estado físico. Cada uno
de los bits de esta porción de memoria estará en el estado que se ha
llamado 1, o en el estado que se ha llamado 0. Y un estado de memoria
codifica una información concreta: la que corresponda al tipo de dato
para el que está reservada esa memoria.

Es conveniente remarcar esta idea. No es necesario, y tampoco lo exige


la sintaxis de C, dar valor inicial a una variable en el momento de su
declaración. La casuística es siempre enorme, y se dan casos y
circunstancias en las que realmente no sea conveniente asignar a la
variable un valor inicial. Pero habitualmente es muy recomendable
inicializar las variables. Otros lenguajes lo hacen por defecto en el
momento de la declaración de variables; C no lo hace. Otros lenguajes
detectan como error de compilación (errar sintáctico) el uso de una
variable no inicializada; C acepta esta posibilidad.

A partir del momento en que se ha declarado esa variable, puede ya


hacerse uso de ella. Tras la declaración ha quedado reservado un
espacio de memoria para almacenar la información de esa variable.

Si declaramos tipo variable = valor; tendremos la variable <variable,


tipo, R, valor>, de la que desconocemos su dirección de memoria. Cada
vez que el programa trabaje con variable estará haciendo referencia a

21
Fundamentos de informática. Programación en Lenguaje C

esta posición de memoria R. Y estará refiriéndose a uno o más bytes, en


función del tamaño del tipo de dato para el que se ha creado la variable.

Tipos de datos primitivos en C: sus dominios.


Los tipos de dato primitivos en C quedan recogidos en la tabla 2.1.

Las variables de tipo de dato carácter ocupan 1 byte. Aunque están


creadas para almacenar caracteres mediante una codificación como la
ASCII (que asigna a cada carácter un valor numérico codificado con esos
8 bits), también pueden usarse como variables numéricas. En ese caso,
el rango de valores es el recogido en la tabla 2.1. En el caso de que se
traten de variables con signo, entonces el rango va desde −27 hasta
2 −1.
7

RANGO DE VALORES
TIPOS SIGNO MENOR MAYOR

tipos de dato CARÁCTER: char

signed -128 +127


char
unsigned 0 +255

tipos de dato ENTERO: int

signed -32.768 +32.767


short
unsigned 0 +65.535
signed -2.147.483.648 +2.147.483.647
long
unsigned 0 4.294.967.295

tipos de dato CON COMA FLOTANTE

float -3.402923E+38 +3.402923E+38


double -1.7976931E+308 +1.7976931E+308
long double -1.2E+4932 +1.2E+4932

Tabla 2.1.: Tipos de datos primitivos en C.

Para crear una variable de tipo carácter en C, utilizaremos la palabra


clave char. Si la variable es con signo, entonces su tipo será signed
char, y si no debe almacenar signo, entonces será unsigned char. Por

22
Capítulo 2. Tipos de datos y variables en C.

defecto, si no se especifica si la variable es con o sin signo, el lenguaje C


considera que se ha tomado la variable con signo, de forma que decir
char es lo mismo que decir signed char.

Lo habitual será utilizar variables tipo char para el manejo de


caracteres. Los caracteres simples del alfabeto latino se representan
mediante este tipo de dato. El dominio de las variables char es un
conjunto finito ordenado de caracteres, para el que se ha definido una
correspondencia que asigna, a cada carácter del dominio, un código
binario diferente de acuerdo con alguna normalización. El código más
extendido es el código ASCII (American Standard Code for Information
Interchange).

Las variables tipo entero, en C, se llaman int. Dependiendo de que esas


variables sean de dos bytes o de cuatro bytes las llamaremos de tipo
short int (16 bits) ó de tipo long int (32 bits). Y para cada una de
ellas, se pueden crear con signo o sin signo: signed short int y signed
long int, ó unsigned short int y unsigned long int. De nuevo, si no
se especifica nada, C considera que la variable entera creada es con
signo, de forma que la palabra signed vuelve a ser opcional. En
general, se recomienda el uso de la palabra signed. Utilizar esa palabra
al declarar enteros con signo facilita la compresión del código.

El tamaño de la variable int depende del concepto, no introducido hasta


el momento, de longitud de la palabra. Habitualmente esta longitud
se toma múltiplo de 8, que es el número de bits del byte. De hecho la
longitud de la palabra viene definido por el máximo número de bits que
puede manejar el procesador, de una sola vez, cuando hace cálculos con
enteros.

Si se declara una variable en un PC como de tipo int (sin determinar si


es short o long), el compilador de C considerará que esa variable es de
la longitud de la palabra: de 16 o de 32 bits. Es importante conocer ese
dato (que depende del compilador), o a cambio es mejor especificar

23
Fundamentos de informática. Programación en Lenguaje C

siempre en el programa si se desea una variable corta o larga, y no


dejar esa decisión al tamaño de la palabra.

Una variable declarada como de tipo long se entiende que es long int.
Y una variable declarada como de tipo short, se entiende que es short
int. Muchas veces se toma como tipo de dato únicamente el modificador
de tamaño, omitiendo la palabra clave int.

Los restantes tipos de dato se definen para codificar valores reales. Hay
que tener en cuenta que el conjunto de los reales es no numerable
(entre dos reales siempre hay un real y, por tanto, hay infinitos reales).
Los tipos de dato que los codifican ofrecen una codificación finita sí
numerable. Esos tipos de dato codifican subconjuntos del conjunto de
los reales; subconjuntos que, en ningún caso, pueden tomarse como un
intervalo del conjunto de los reales.

A esta codificación de los reales se le llama de coma flotante. Así


codifica el lenguaje C (y muchos lenguajes) los valores no enteros.
Tomando como notación para escribir esos números la llamada notación
científica (signo, mantisa, base, exponente: por ejemplo, el numero de
Avogadro, +6, 023 ⋅ 1023 : signo positivo, mantisa 6,023, base decimal y
exponente 23), almacena en memoria, de forma normalizada (norma
IEEE754) el signo del número, su mantisa y su exponente. No es
necesario almacenar la base, que en todos los casos trabaja en la base
binaria.

Los tipos de dato primitivos en coma flotante que ofrece el lenguaje C


son tres: float, que reserva 4 bytes para su codificación y que toma
valores en el rango señalado en la tabla 2.1.; double, que reserva 8
bytes; y long double, que reserva 10 bytes. Desde luego, en los tres
tipos de dato el dominio abarca tantos valores positivos como negativos.

Existe además un tipo de dato que no reserva espacio en memoria: su


tamaño es nulo. Es el tipo de dato void. No se pueden declarar
variables de ese tipo. Más adelante se verá la necesidad y utilidad de

24
Capítulo 2. Tipos de datos y variables en C.

tener definido un tipo de dato de estas características. Por ejemplo es


muy conveniente para el uso de funciones.

En C el carácter que indica el fin de la parte entera y el comienzo de la


parte decimal se escribe mediante el carácter punto. La sintaxis no
acepta interpretaciones de semejanza, y para el compilador el carácter
coma es un operador que nada tiene que ver con el punto decimal. Una
equivocación en ese carácter causará habitualmente un error en tiempo
de compilación.

El lenguaje C dedica nueve palabras para la identificación de los tipos de


dato primitivos: void, char, int, float, double, short, long, signed e
unsigned. Ya se ha visto, por tanto más de un 25 % del léxico total del
lenguaje C. Existen más palabras clave para la declaración y creación de
variables. Se verán más adelante.

Tipos de datos primitivos en C: sus operadores.


Ya se dijo que un tipo de dato explicita un conjunto de valores, llamado
dominio, sobre el que son aplicables una serie de operadores. No queda
del todo definido un tipo de dato presentando sólo su dominio. Falta
indicar cuáles son los operadores que están definidos para cada tipo de
dato.

Los operadores pueden aplicarse a una sola variable, a dos variables, e


incluso a varias variables. Llamamos operación unaria a la que se
aplica a una sola variable. Una operación unaria es, por ejemplo, el
signo del valor.

No tratamos ahora las operaciones que se pueden aplicar sobre una


variable creada para almacenar caracteres. Más adelante hay un
capítulo entero dedicado a este tipo de dato char.

Las variables enteras, y las char cuando se emplean como variables


enteras de pequeño rango, además del operador unario del signo, tienen

25
Fundamentos de informática. Programación en Lenguaje C

definidos el operador asignación, los operadores aritméticos, los


relacionales y lógicos y los operadores a nivel de bit.

Los operadores de las variables con coma flotante son el operador


asignación, todos los aritméticos (excepto el operador módulo o cálculo
del resto de una división), y los operadores relacionales y lógicos. Las
variables float, double y long double no aceptan el uso de operadores
a nivel de bit.

Operador asignación.
El operador asignación permite al programador modificar los valores de
las variables y alterar, por tanto, el estado de la memoria del ordenador.

El carácter que representa al operador asignación es el carácter ‘=’. La


forma general de este operador es

nombre_variable = expresión;

Donde expresión puede ser un literal, otra variable, o una combinación


de variables, literales y operadores y funciones. Podemos definirlo como
una secuencia de operandos y operadores que unidos según ciertas
reglas producen un resultado.

Este signo en C no significa igualdad en el sentido matemático al que


estamos acostumbrados, sino asignación. No puede llevar a equívocos
expresiones como la siguiente:

a = a + 1;

Ante esta instrucción, el procesador toma el valor de la variable a, lo


copia en un registro de la ALU donde se incrementa en una unidad, y
almacena (asigna) el valor resultante en la variable a, modificando por
ello el valor anterior de esa posición de memoria. La expresión
comentada no es una igualdad de las matemáticas, sino una orden para
incrementar en uno el valor almacenado en la posición de memoria
reservada por la variable a.

26
Capítulo 2. Tipos de datos y variables en C.

El operador asignación tiene dos extremos: el izquierdo (que toma el


nombre Lvalue en mucho manuales) y el derecho (Rvalue). La
apariencia del operador es, entonces:

LValue = RValue;

Donde Lvalue sólo puede ser el nombre de una variable, y nunca una
expresión, ni un literal. Expresiones como a + b = c; ó 3 = variable; son
erróneas, pues ni se puede cambiar el valor del literal 3 que, además,
no está en memoria porque es un valor literal; ni se puede almacenar
un valor en la expresión a + b, porque los valores se almacenan en
variables, y a + b no es variable alguna.

Un error de este estilo interrumpe la compilación del programa. El


compilador dará un mensaje de error en el que hará referencia a que el
Lvalue de la asignación no es correcto.

Cuando se trabaja con variables enteras, al asignar a una variable un


valor mediante un literal (por ejemplo, v = 3;) se entiende que ese dato
viene expresado en base 10.

Pero en C es posible asignar valores en la base hexadecimal. Si se


quiere dar a una variable un valor en hexadecimal, entonces ese valor
va precedido de un cero y una letra equis. Por ejemplo, si se escribe v =
0x20, se está asignando a la variable v el valor 20 en hexadecimal, es
decir, el 32 en decimal, es decir, el valor 100000 en binario.

Operadores aritméticos.
Los operadores aritméticos son:

1. Suma. El identificador de este operador es el carácter ‘+’. Este


operador es aplicable sobre cualquier variable primitiva de C. Si el
operador ‘+’ se emplea como operador unario, entonces es el
operador de signo positivo.

27
Fundamentos de informática. Programación en Lenguaje C

2. Resta. El identificador de este operador es el carácter ‘– ‘. Este


operador es aplicable sobre cualquier variable primitiva de C. Si el
operador ‘–’ se emplea como operador unario, entonces es el
operador de signo negativo.

3. Producto. El identificador de este operador es el carácter ‘*’. Este


operador es aplicable sobre cualquier variable primitiva de C.

4. Cociente o División. El identificador de este operador es el carácter


‘/’. Este operador es aplicable sobre cualquier variable primitiva de
C.

Cuando el cociente se realiza con variables enteras el resultado será


también un entero, y trunca el resultado al mayor entero menor que
el cociente. Por ejemplo, 5 dividido entre 2 es igual a 2. Y 3 dividido
entre 4 es igual a 0. Es importante tener esto en cuenta cuando se
trabaja con enteros.

Supongamos la expresión

sup = (1 / 2) * base * altura;

para el cálculo de la superficie de un triángulo, y supongamos que


todas las variables que intervienen han sido declaradas enteras. Así
expresada la sentencia o instrucción de cálculo, el resultado será
siempre el valor 0 para la variable sup, sea cual sea el valor actual
de las variable base o altura: y es que al calcular el valor de 1
dividido entre 2, el procesador ofrece como resultado el valor 0.

Cuando el cociente se realiza entre variables de coma flotante,


entonces el resultado es también de coma flotante.

Siempre se debe evitar el cociente en el que el denominador sea


igual a cero, porque en ese caso se dará un error de ejecución y el
programa quedará abortado.

5. Módulo. El identificador de este operador es el carácter ‘%’. Este


operador calcula el resto del cociente entero. Por su misma

28
Capítulo 2. Tipos de datos y variables en C.

definición, no tiene sentido su aplicación entre variables no enteras:


su uso con variables de coma flotante provoca error de compilación.
Como en el cociente, tampoco su divisor puede ser cero.

6. Incremento y decremento. Estos dos operadores no existen en


otros lenguajes. El identificador de estos operadores son los
caracteres “++” para el incremento, y “--” para el decremento. Este
operador es válido para todos los tipos de dato primitivos de C.

La expresión “a++;” es equivalente a la expresión “a = a + 1;”. Y la


expresión “a--;” es equivalente a la expresión “a = a - 1;”.

Estos operadores condensan, en uno sola expresión, un operador


asignación, un operador suma (o resta) y un valor literal: el valor 1.
Y como se puede apreciar son operadores unarios: se aplican a una
sola variable.

Dónde se ubique el operador con respecto a la variable tiene su


importancia, porque varía su comportamiento dentro del total de la
expresión.

Por ejemplo, el siguiente código


unsigned short int a, b = 2, c = 5;
a = b + c++;

modifica dos variables: por el operador asignación, la variable a


tomará el valor resultante de sumar los contenidos de b y c; y por la
operación incremento, que lleva consigo asociado otro operador
asignación, se incrementa en uno el valor de la variable c.

Pero queda una cuestión abierta: ¿Qué operación se hace primero:


incrementar c y luego calcular b + c para asignar su resultado a la
variable a; o hacer primero la suma y sólo después incrementar la
variable c?

Eso lo indicará la posición del operador. Si el operador incremento (o


decremento) precede a la variable, entonces se ejecuta antes de
evaluar el resto de la expresión; si se coloca después de la variable,

29
Fundamentos de informática. Programación en Lenguaje C

entonces primero se evalúa la expresión donde está implicada la


variable a incrementar o decrementar y sólo después se incrementa
o decrementa esa variable.

En el ejemplo antes sugerido, el operador está ubicado a la derecha


de la variable c. Por lo tanto, primero se efectúa la suma y la
asignación sobre a, que pasa a valer 7; y luego se incrementa la
variable c, que pasa a valer 6. La variable b no modifica su valor.

Por completar el ejemplo, si la expresión hubiera sido

a = b + ++c;

entonces, al final tendríamos que c vale 6 y que a vale 8, puesto que


no se realizaría la suma y la asignación sobre a hasta después de
haber incrementado el valor de la variable c.

Los operadores incremento y decremento, y el juego de la


precedencia, son muy cómodos y se emplean mucho en los códigos
escritos en lenguaje C.

Aunque hasta el tema siguiente no se va a ver el modo en que se


pueden recibir datos desde el teclado (función scanf()) y el modo de
mostrar datos por pantalla (función printf()), vamos a recoger a lo
largo de este capítulo algunas cuestiones muy sencillas para
resolver. Por ahora lo importante no es entender el programa entero,
sino la parte que hace referencia a la declaración y uso de las
variables.

Operadores relacionales y lógicos.


Los operadores relacionales y los operadores lógicos crean expresiones
que se evalúan como verdaderas o falsas.

En muchos lenguajes existe un tipo de dato primitivo para estos valores


booleanos de verdadero o falso. En C ese tipo de dato no existe.

30
Capítulo 2. Tipos de datos y variables en C.

El lenguaje C toma como falsa cualquier expresión que se evalúe como


0. Y toma como verdadera cualquier otra evaluación de la expresión. Y
cuando en C se evalúa una expresión con operadores relacionales y/o
lógicos, la expresión queda evaluada a 0 si el resultado es falso; y a 1 si
el resultado es verdadero.

Los operadores relacionales son seis: igual que (“==”), distintos


(“!=”) , mayor que (‘>’), mayor o igual que (“>=”), menor que (‘<’)
y menor o igual que (“<=”).

Todos ellos se pueden aplicar a cualquier tipo de dato primitivo de C.

Una expresión con operadores relacionales sería, por ejemplo, a != 0,


que será verdadero si a toma cualquier valor diferente al 0, y será falso
si a toma el valor 0. Otras expresiones relacionales serían, por ejemplo,

a > b + 2;

x + y == z + t;

Con frecuencia interesará evaluar una expresión en la que se obtenga


verdadero o falso no solo en función de una relación, sino de varias. Por
ejemplo, se podría necesitar saber (obtener verdadero o falso) si el valor
de una variable concreta está entre dos límites superior e inferior. Para
eso necesitamos concatenar dos relacionales. Y eso se logra mediante
los operadores lógicos.

Un error frecuente (y de graves consecuencias en la ejecución del


programa) al programar en C ó C++ es escribir el operador asignación
(‘=’), cuando lo que se pretendía escribir era el operador relacional
“igual que” (“==”). El C ó C++ la expresión variable = valor; será
siempre verdadera si valor es distinto de cero. Si colocamos una
asignación donde deseábamos poner el operador relacional “igual que”,
tendremos dos consecuencias graves: se cambiará el valor de la variable
colocada a la izquierda del operador asignación (cosa que no queríamos)

31
Fundamentos de informática. Programación en Lenguaje C

y, si el valor de la variable de la derecha es distinto de cero, la


expresión se evaluará como verdadera al margen de cuáles fueran los
valores iniciales de las variables.

Los operadores lógicos son: AND, cuyo identificador está formado por el
carácter repetido “&&”; OR, con el identificador “||”; y el operador
negación, cuyo identificador es el carácter de admiración final (‘!’).

a b a && b a || b !a

F F F F V
F V F V V
V F F V F
V V V V F

Tabla 2.2.: Resultados de los


operadores lógicos.

Estos operadores binarios actúan sobre dos expresiones que serán


verdaderas (o distintas de cero), o falsas (o iguales a cero), y devuelven
como valor 1 ó 0 dependiendo de que la evaluación haya resultado
verdadera o falsa.

La tabla de valores para conocer el comportamiento de estos operadores


está recogida en la tabla 2.2. En esa tabla se recoge el resultado de los
tres operadores en función del valor de cada una de las dos expresiones
que evalúan.

Por ejemplo, supongamos el siguiente código en C:


int a = 1 , b = 3 , x = 30 , y = 10;
int resultado;
resultado = a * x == b * y;

El valor de la variable resultado quedará igual a 1.

Y si queremos saber si la variable x guarda un valor entero positivo


menor que cien, escribiremos la expresión

(x > 0 && x < 100)

32
Capítulo 2. Tipos de datos y variables en C.

Con estos dos grupos de operadores son muchas las expresiones de


evaluación que se pueden generar. Quizá en este momento no adquiera
mucho sentido ser capaz de expresar algo así; pero más adelante se
verá cómo la posibilidad de verificar sobre la veracidad y falsedad de
muchas expresiones permite crear estructuras de control condicionales,
o de repetición.

Una expresión con operadores relacionales y lógicos admite varias


formas equivalentes Por ejemplo, la antes escrita sobre el intervalo de
situación del valor de la variable x es equivalente a escribir

!(x < 0 || x >= 100)

• Evaluar las siguientes expresiones.


short a = 0, b = 1, c = 5;
a; // FALSO
b; // VERDADERO
a < b; // VERDADERO
5 * (a + b) == c; // VERDADERO

float pi = 3.141596;
long x = 0, y = 100, z =1234;

3 * pi < y && (x + y) * 10 <= z / 2; // FALSO


3 * pi < y || (x + y) * 10 <= z / 2; // VERDADERO
3 * pi < y && !((x + y) * 10 <= z / 2); // VERDADERO

long a = 5, b = 25, c = 125, d = 625;


5 * a == b; // VERDADERO
5 * b == c; // VERDADERO
a + b + c + d < 1000; // VERDADERO
a > b || a = 10; // VERDADERO

La última expresión trae su trampa: Por su estructura se ve que se ha


pretendido crear una expresión lógica formada por dos sencillas
enlazadas por el operador OR. Pero al establecer que uno de los
extremos de la condición es a = 10 (asignación, y no operador relacional
“igual que”) se tiene que en esta expresión recogida la variable a pasa a

33
Fundamentos de informática. Programación en Lenguaje C

valer 10 y la expresión es verdadera puesto que el valor 10 es


verdadero (todo valor distinto de cero es verdadero).

Operadores a nivel de bit.


Ya se ha dicho en el capítulo primero que el lenguaje C es de medio
nivel. Con eso se quiere decir que es un lenguaje de programación que
tiene la capacidad de trabajar a muy bajo nivel, modificando un bit de
una variable, o logrando códigos que manipulan la codificación interna
de la información. Todos los operadores a nivel de bit están definidos
únicamente sobre variables de tipo entero. No se puede aplicar sobre
una variable float, ni sobre una double, ni sobre una long double.

Los operadores a nivel de bit son seis:

1. Operador AND a nivel de bit. Su identificador es un solo carácter


‘&’. Se aplica sobre variables del mismo tipo, con la misma longitud
de bits. Bit a bit compara los dos de cada misma posición y asigna al
resultado un 1 en ese bit en esa posición si los dos bits de las dos
variables sobre las que se opera valen 1; en otro caso asigna a esa
posición del bit el valor 0.

2. Operador OR a nivel de bit. Su identificador es un solo carácter ‘|’.


Se aplica sobre variables del mismo tipo, con la misma longitud de
bits. Bit a bit compara los dos de cada misma posición y asigna al
resultado un 1 en ese bit en esa posición si alguno de los dos bits de
las dos variables sobre las que se opera valen 1; si ambos bits valen
cero, asigna a esa posición del bit el valor 0.

Es frecuente en C y C++ el error de pretender escribir el operador


lógico “and” (“&&”), o el “or” (“||”) y escribir finalmente el operador
a nivel de bit (‘&’ ó ‘|’). Desde luego el significado de la sentencia o
instrucción será completamente distinto e imprevisible. Será un error
del programa de difícil detección.

34
Capítulo 2. Tipos de datos y variables en C.

3. Operador OR EXCLUSIVO, ó XOR a nivel de bit. Su identificador


es un carácter ‘^’. Se aplica sobre variables del mismo tipo, con la
misma longitud de bits. Bit a bit compara los dos de cada misma
posición y asigna al resultado un 1 en ese bit en esa posición si los
dos bits de las dos variables tienen valores distintos: el uno es 1 y el
otro 0, o viceversa; si los dos bits son iguales, asigna a esa posición
del bit el valor 0.

variable binario hex. dec.

a 1010 1011 1100 1101 ABCD 43981


b 0110 0111 1000 1001 6789 26505
a_and_b 0010 0011 1000 1001 2389 9097
a_or_b 1110 1111 1100 1101 EFCD 61389
a_xor_b 1100 1100 0100 0100 CC44 52292

Tabla 2.3.: Valores del ejemplo en binario, hexadecimal y


decimal. Operadores a nivel de bit.

Por ejemplo, y antes de seguir con los otros tres operadores a nivel
de bit, supongamos que tenemos el siguiente código:
unsigned short int a = 0xABCD, b = 0x6789;
unsigned short int a_and_b = a & b;
unsigned short int a_or_b = a | b;
unsigned short int a_xor_b = a ^ b;

La variable a vale, en hexadecimal ABCD, y en decimal 43981. La


variable b 6789, que en base diez es 26505. Para comprender el
comportamiento de estos tres operadores, se muestra ahora en la
tabla 2.3. los valores de a y de b en base dos, donde se puede ver
bit a bit de ambas variables, y veremos también el bit a bit de las
tres variables calculadas.

En la variable a_and_b se tiene un 1 en aquellas posiciones de bit


donde había 1 en a y en b; un 0 en otro caso. En la variable a_or_b
se tiene un 1 en aquellas posiciones de bit donde había al menos un
1 entre a y b; un 0 en otro caso. En la variable a_xor_b se tiene un 1
en aquellas posiciones de bit donde había un 1 en a y un 0 en b, o

35
Fundamentos de informática. Programación en Lenguaje C

un 0 en a y un 1 en b; y un cero cuando ambos bits coincidían de


valor en esa posición.

La tabla de valores de estos tres operadores queda recogida en la


tabla 2.4.

and or xor

0 0 0 0 0
0 1 0 1 1
1 0 0 1 1
1 1 1 1 0

Tabla 2.4.: Valores que


adoptan los tres operadores a
nivel de bit

4. Operador complemento a uno. Este operador unario devuelve el


complemento a uno del valor de la variable a la que se aplica. Su
identificador es el carácter ‘~’. Si se tiene que a la variable x de tipo
short se le asigna el valor hexadecimal ABCD, entonces la variable
y, a la que se asigna el valor ~x valdrá 5432. O si x vale 578D,
entonces y valdrá A872. Puede verificar estos resultados calculando
los complementos a uno de ambos números.

5. Operador desplazamiento a izquierda. Su identificador es la


cadena “<<”. Es un operador binario, que realiza un desplazamiento
de todos los bits de la variable o valor literal sobre la que se aplica
un número dado de posiciones hacia la izquierda. Los bits más a la
izquierda (los más significativos) se pierden; a la derecha se van
introduciendo tantos bits puestos a cero como indique el
desplazamiento.

Por ejemplo, si tenemos el siguiente código:


short int var1 = 0x7654;
short int var2 = var1 << 3;

36
Capítulo 2. Tipos de datos y variables en C.

La variable var2 tendrá el valor de la variable var1 a la que se le


aplica un desplazamiento a izquierda de 3 bits.

Si la variable var1 tiene el valor, en base binaria

0111 0110 0101 0100 (estado de la variable var1)

entonces la variable var2, a la que se asigna el valor de la variable


var1 al que se han añadido tres ceros a su derecha y se le han
eliminado los tres dígitos más a la izquierda queda:

1011 0010 1010 0000 (estado de la variable var2).

Es decir, var2 valdrá, en hexadecimal, B2A0.

Una observación sobre esta operación. Introducir un cero a la


derecha de un número es lo mismo que multiplicarlo por la base.

En el siguiente código
unsigned short int var1 = 12;
unsigned short int d = 1;
unsigned short int var2 = var1 << d;

var2 será el doble que var1, es decir, 24. Y si d hubiera sido igual a
2, entonces var2 sería cuatro veces var1, es decir, 48. Y si d hubiera
sido igual a 3, entonces var2 sería ocho veces var1, es decir, 96.

Si llega un momento en que el desplazamiento obliga a perder algún


dígito 1 a la izquierda, entonces ya habremos perdido esa
progresión, porque la memoria no será suficiente para albergar todos
sus dígitos y la cifra será truncada.

Si las variables var1 y var2 están declaradas como signed, y si la


variable var1 tiene asignado un valor negativo (por ejemplo, -7),
también se cumple que el desplazamiento equivalga a multiplicar por
dos. Es buen ejercicio de cálculo de complementos y de codificación
de enteros con signo verificar lo datos que a continuación se
presentan:

var1 = -7;: estado de memoria FFF9

37
Fundamentos de informática. Programación en Lenguaje C

var2 = var1 << 1;: estado de memoria para la variable var2 será
FFF2, que es la codificación del entero -14.

6. Operador desplazamiento a derecha. Su identificador es la cadena


“>>”. Es un operador binario, que realiza un desplazamiento de
todos los bits de la variable o valor literal sobre la que se aplica un
número dado de posiciones hacia la derecha. Los bits más a la
derecha (los menos significativos) se pierden; a la izquierda se van
introduciendo tantos bits como indique el desplazamiento. En esta
ocasión, el valor de los bits introducidos por la izquierda dependerá
del signo del entero sobre el que se aplica el operador
desplazamiento. Si el entero es positivo, se introducen tantos ceros
por la izquierda como indique el desplazamiento. Si el entero es
negativo, se introducen tantos unos por la izquierda como indique el
desplazamiento. Evidentemente, si el entero sobre el que se aplica el
desplazamiento a derecha está declarado como unsigned,
únicamente serán ceros lo que se introduzca por su izquierda, puesto
que en ningún caso puede codificar un valor negativo.

Si desplazar a la izquierda era equivalente a multiplicar por la base,


ahora, desplazar a la derecha es equivalente a dividir por la base
(división entera, sesgando el valor al entero mayor, menor que el
resultado de dicho cociente). Y el hecho de que el desplazamiento a
derecha considere el signo en el desplazamiento permite que, en los
valores negativos, siga siendo equivalente desplazar a derecha que
dividir por la base.

Por ejemplo, si tenemos el siguiente código


signed short int var1 = -231;
signed short int var2 = var1 >> 1;

Entonces, el estado que codifica el valor de var1 es, expresado en


hexadecimal, FF19. Y el valor que codifica entonces var2, si lo hemos
desplazado un bit a la derecha, será FF8C, que es la codificación del
entero negativo -116.

38
Capítulo 2. Tipos de datos y variables en C.

Los operadores a nivel de bit tienen una gran potencialidad. De todas


formas no son operaciones a las que se está normalmente habituado, y
eso hace que no resulte muchas veces evidente su uso. Los operadores
a nivel de bit operan a mucha mayor velocidad que, por ejemplo, el
operador producto o cociente. En la medida en que se sabe, quien
trabaja haciendo uso de esos operadores puede lograr programas
notablemente más veloces en ejecución.

Operadores compuestos.
Ya se ha visto que una expresión de asignación en C trae, a su izquierda
(Lvalue), el nombre de una variable y, a su derecha (Rvalue) una
expresión a evaluar, o un literal, o el nombre de otra variable. Y ocurre
frecuentemente que la variable situada a la izquierda forma parte de la
expresión de la derecha. En estos casos, y si la expresión es sencilla,
todos los operadores aritméticos y los operadores a nivel de bit binarios
(exceptuando, por tanto, los operadores de signo, incremento,
decremento y complemento) pueden presentar otra forma, en la que se
asocia el operador con el operador asignación. Son los llamados
operadores de asignación compuestos:
+= x += y; es lo mismo que decir x = x + y;
–= x -= y; es lo mismo que decir x = x – y;
*= x *= y; es lo mismo que decir x = x * y;
/= x /= y; es lo mismo que decir x = x / y;
%= x %= y; es lo mismo que decir x = x % y;
>>= x >>= y; es lo mismo que decir x = x >> y;
<<= x <<= y; es lo mismo que decir x = x << y;
&= x &= y; es lo mismo que decir x = x & y;
|= x |= y; es lo mismo que decir x = x | y;
^= x ^= y; es lo mismo que decir x = x ^ y;

Puede parecer que estos operadores no facilitan la comprensión del


código escrito. Quizá una expresión de la forma F*=n--; no tenga una
presentación muy clara. Pero de hecho estos operadores compuestos se
usan frecuentemente y, quien se habitúa a ellos, agradece que se hayan
definido para el lenguaje C.

39
Fundamentos de informática. Programación en Lenguaje C

Por cierto, que la expresión del párrafo anterior es equivalente a escribir


estas dos líneas de código: F = F * n; y n = n – 1;.

Operador sizeof.
Ya sabemos el número de bytes que ocupan en memoria todas las
variables de tipo de dato primitivo en C: 1 byte las variables tipo char;
2 las de tipo short; 4 las de tipo long y float; 8 las de tipo double, y
10 las variables long double.

Pero ya se ha dicho que además de estos tipos primitivos, C permite la


definición de otros tipos diferentes, combinación de esos primitivos. Y
los tamaños de esos tipos definidos pueden ser tan diversos como
diversas pueden ser las definiciones de esos nuevos tipos. no es extraño
trabajar con tipos cuyas variables ocupan 13 bytes, ó 1045,ó cualquier
otro tamaño.

C ofrece un operador que devuelve la cantidad de bytes que ocupa una


variable o un tipo de dato concreto. El valor devuelto es tomado como
un entero, y puede estar presente en cualquier expresión de C. Es el
operador sizeof. Su sintaxis es:

sizeof(nombre_variable); ó sizeof(nombre_tipo_de_dato);

ya que se puede utilizar tanto con una variable concreta como


indicándole al operador el nombre del tipo de dato sobre el que
queramos conocer su tamaño. No es válido utilizar este operador
indicando entre paréntesis el tipo de dato void: esa instrucción daría
error en tiempo de compilación.

Con este operador aseguramos la portabilidad, al no depender la


aplicación del tamaño del tipo de datos de la máquina que se vaya a
usar. Aunque ahora mismo no se ha visto en este texto qué utilidad
puede tener en un programa conocer, como dato de cálculo, el número

40
Capítulo 2. Tipos de datos y variables en C.

de bytes que ocupa una variable, la verdad es que con frecuencia ese
dato es muy necesario.

Ejemplo: Podemos ver el tamaño de los diferentes tipos de datos


primitivos de C. Basta teclear este código en nuestro editor:
#include <stdio.h>
main()
{
printf("int => %d\n",sizeof(int));
printf("char => %d\n",sizeof(char));
printf("short => %d\n",sizeof(short));
printf("long => %d\n",sizeof(long));
printf("float => %d\n",sizeof(float));
printf("double => %d\n",sizeof(double));
}

(La función printf quedará presentada y explicada en el próximo


capítulo.)

Expresiones en las que intervienen variables de


diferente tipo.
Hay lenguajes de programación que no permiten realizar operaciones
con valores de tipos de dato distintos. Se dice que son lenguajes de
tipado fuerte, que fuerzan la comprobación de la coherencia de tipos en
todas las expresiones, y lo verifican en tiempo de compilación.

El lenguaje C NO es de esos lenguajes, y permite la compilación de un


programa con expresiones que mezclan los tipos de datos.

Y aunque en C se pueden crear expresiones en las que intervengan


variables y literales de diferente tipo de dato, el procesador trabaja de
forma que todas las operaciones que se realizan en la ALU sean con
valores del mismo dominio.

Para lograr eso, cuando se mezclan en una expresión diferentes tipos de


dato, el compilador convierte todas las variables a un único tipo
compatible; y sólo después de haber hecho la conversión se realiza la
operación.

41
Fundamentos de informática. Programación en Lenguaje C

Esta conversión se realiza de forma que se no se pueda perder


información: en una expresión donde intervienen elementos de
diferentes dominios, todos los valores se codifican de acuerdo con el tipo
de dato de mayor rango.

La ordenación de rango de los tipos de dato primitivos de C es, de


menor a mayor, la siguiente:

char – short – long – float – double – long double

Así, por ejemplo, si se presenta el siguiente código:


char ch = 7;
short sh = 2;
long ln = 100, ln2;
double x = 12.4, y;
y = (ch * ln) / sh – x;

La expresión para el cálculo que se almacenará en la variable y va


cambiando de tipo de dato a medida que se va realizando: en el
producto de la variable char con la variable long, se fuerza el cambio
de la variable de tipo char, que se recodificará y así quedará para su
uso en la ALU, a tipo long. Esa suma será por tanto un valor long.
Luego se realizará el cociente con la variable short, que deberá
convertirse en long para poder dividir al resultado long antes obtenido.
Y, finalmente, el resultado del cociente se debe convertir en un valor
double, para poder restarle el valor de la variable x.

El resultado final será pues un valor del tipo de dato double. Y así será
almacenado en la posición de memoria de la variable y de tipo double.

Si la última instrucción hubiese sido

ln2 = (ch * ln) / sh – x;

todo hubiera sido como se ha explicado, pero a la hora de almacenar la


información en la memoria reservada para la variable long ln2, el
resultado final, que venía expresado en formato double, deberá
recodificarse para ser guardado como long. Y es que, en el trasiego de
la memoria a los registros de la ALU, bien se puede hacer un cambio de

42
Capítulo 2. Tipos de datos y variables en C.

tipo y por tanto un cambio de forma de codificación y, especialmente, de


número de bytes empleados para esa codificación. Pero lo que no se
puede hacer es que en una posición de memoria como la del ejemplo,
que dedica 32 bits a almacenar información, se quiera almacenar un
valor de 64 bits, que es lo que ocupan las variables double.

Ante el operador asignación, si la expresión evaluada, situada en la


parte derecha de la asignación, es de un tipo de dato diferente al tipo de
dato de la variable indicada a la izquierda de la asignación, entonces el
valor del lado derecho de la asignación se convierte al tipo de dato del
lado izquierdo. En este caso el forzado de tipo de dato puede consistir
en llevar un valor a un tipo de dato de menor rango. Y ese cambio corre
el riesgo de perder —truncar— la información.

Operador para forzar cambio de tipo.


En el epígrafe anterior se ha visto el cambio o conversión de tipo de
dato que se realiza de forma implícita en el procesador cuando
encuentra expresiones que contienen diferentes tipos de dato. También
existe una forma en que programador puede forzar un cambio de tipo de
forma explícita. Este cambio se llama cambio por promoción, o casting.
C dispone de un operador para forzar esos cambios.

La sintaxis de este operador unario es la siguiente:

(tipo) nombre_variable;

El operador de promoción de tipo, o casting, precede a la variable. Se


escribe entre paréntesis el nombre del tipo de dato hacia donde se
desea forzar el valor codificado en la variable sobre la que se aplica el
operador.

La operación de conversión debe utilizarse con cautelas, de forma que


los cambios de tipo sean posibles y compatibles. No se puede realizar
cualquier cambio. Especialmente cuando se trabaja con tipos de dato

43
Fundamentos de informática. Programación en Lenguaje C

creados (no primitivos), que pueden tener una complejidad grande.


También hay que estar vigilante a los cambios de tipo que fuerzan a una
disminución en el rango: por ejemplo, forzar a que una variable float
pase a ser de tipo long. El rango de valores de una variable float es
muchos mayor, y si el valor de la variable es mayor que el valor máximo
del dominio de los enteros de 4 bytes, entonces el resultado del
operador forzar tipo será imprevisible. Y tendremos entonces una
operación que no ofrece problema alguno en tiempo de compilación,
pero que bien puede llevar a resultados equivocados en tiempo de
ejecución.

de tipo… al tipo… Posibles pérdidas


char signed char Si el valor inicial es mayor de
127, entonces el nuevo valor
será negativo.
short char Se pierden los 8 bits más
significativos.
long int char Se pierden los 24 bits más
significativos.
long int short Se pierden los 16 bits más
significativos.
float int Se pierde la parte fraccional y
más información.
double float Se pierde precisión. El
resultado se presenta
redondeado.
long double double Se pierde precisión. El
resultado se presenta
redondeado.

Tabla 2.5.: Pérdidas de información en los cambios de tipo


con disminución de rango.

No se pueden realizar conversiones del tipo void a cualquier otro tipo,


pero sí de cualquier otro tipo al tipo void. Eso se entenderá mejor más
adelante.

En la tabla 2.5. se muestran las posibles pérdidas de información que se


pueden producir en conversiones forzadas de tipo de dato. Esas pérdidas

44
Capítulo 2. Tipos de datos y variables en C.

se darán tanto si la conversión de tipo de dato viene forzada por el


operador conversor de tipo, como si es debido a exigencias del operador
asignación. Evidentemente, salvo motivos intencionados que rara vez se
han de dar, este tipo de conversiones hay que evitarlas. Los
compiladores no interrumpen el trabajo de compilación cuando
descubren, en el código, alguna conversión de esta índole, pero sí
advierten de aquellas expresiones donde puede haber un cambio
forzado de tipo de dato que conduzca a la pérdida de información.

Propiedades de los operadores.


Al evaluar una expresión formada por diferentes variables y literales, y
por diversos operadores, hay que lograr expresar realmente lo que se
desea operar. Por ejemplo, la expresión a + b * c… ¿Se evalúa como el
producto de la suma de a y b, con c; o se evalúa como la suma del
producto de b con c, y a?

Para definir unas reglas que permitan una interpretación única e


inequívoca de cualquier expresión, se han definido tres propiedades en
los operadores:

1. su posición. Establece dónde se coloca el operador con respecto a


sus operandos. Un operador se llamará infijo si viene a colocarse
entre sus operandos; y se llamará prefijo se el operador precede al
operando.

2. su precedencia. Establece el orden en que se ejecutan los distintos


operadores implicados en una expresión. Existe un orden de
precedencia perfectamente definido, de forma que en ningún caso
una expresión puede tener diferentes interpretaciones. Y el
compilador de C siempre entenderá las expresiones de acuerdo con
su orden de precedencia establecido.

3. su asociatividad. Esta propiedad resuelve la ambigüedad en la


elección de operadores que tengan definida la misma precedencia.

45
Fundamentos de informática. Programación en Lenguaje C

En la práctica habitual de un programador, se acude a dos reglas para


lograr escribir expresiones que resulten correctamente evaluadas:

1. Hacer uso de paréntesis. De hecho los paréntesis son un operador


más, que además son los primeros en el orden de ejecución. De
acuerdo con esta regla, la expresión antes recogida podría escribirse
(a + b) * c; ó a + (b * c); en función de cuál de las dos se desea.
Ahora, con los paréntesis, estas expresiones no llevan a equívoco
alguno.

2. Conocer y aplicar las reglas de precedencia y de asociación por


izquierda y derecha. Este orden podrá ser siempre alterado mediante
el uso de paréntesis. Según esta regla, la expresión antes recogida
se interpreta como a + (b * c).

Se considera buena práctica de programación conocer esas reglas de


precedencia y no hacer uso abusivo de los paréntesis. De todas
formas, cuando se duda sobre cómo se evaluará una expresión, lo
habitual es echar mano de los paréntesis. A veces una expresión
adquiere mayor claridad si se recurre al uso de los paréntesis.

Las reglas de precedencia son las que se recogen el la tabla 2.6. Cuanto
más alto en la tabla esté el operador, más alta es su precedencia, y
antes se evalúa ese operador que cualquier otro que esté más abajo en
la tabla. Y para aquellos operadores que estén en la misma fila, es decir,
que tengan el mismo grado de precedencia, el orden de evaluación, en
el caso en que ambos operadores intervengan en una expresión, viene
definido por la asociatividad: de derecha a izquierda o de izquierda a
derecha.

Existen 16 categorías de precedencia, y todos los operadores colocados


en la misma categoría tienen igual precedencia que cualquiera otro de la
misma categoría. Algunas de esas categorías tan solo tienen un
operador.

46
Capítulo 2. Tipos de datos y variables en C.

Cuando un operador viene duplicado en la tabla, la primera ocurrencia


es como operador unario, la segunda como operador binario.

Cada categoría tiene su regla de asociatividad: de derecha a izquierda


(anotada como D – I), o de izquierda a derecha (anotada como I – D).

() [] -> . I–D
! ~ ++ -- + - * & D–I
.* ->* I–D
* / % I–D
+ - I–D
<< >> I–D
> >= < <= I–D
== != I–D
& I–D
^ I–D
| I–D
&& I–D
|| D–I
?: I–D
= += -= *= /= %= &= |= <<= >>= D–I
, I–D

Tabla 2.6.: Precedencia y Asociatividad de los operadores.

Por ejemplo, la expresión

a * x + b * y – c / z:

se evalúa en el siguiente orden: primero los productos y el cociente, y


ya luego la suma y la resta. Todos estos operadores están en categorías
con asociatividad de izquierda a derecha, por lo tanto, primero se
efectúa el producto más a la izquierda y luego el segundo, más al centro

47
Fundamentos de informática. Programación en Lenguaje C

de la expresión. Después se efectúa el cociente; luego la suma y


finalmente la resta.

Todos los operadores de la tabla 2.6. que faltan por presentar en el


manual se emplean para vectores y cadenas y para operatoria de
punteros. Más adelante se conocerán todos ellos.

Valores fuera de rango en una variable.


Ya hemos dicho repetidamente que una variable es un espacio de
memoria, de un tamaño concreto en donde la información se codifica de
una manera determinada por el tipo de dato que se vaya a almacenar
en esa variable.

Espacio de memoria limitado.

Es importante conocer los límites de nuestras variables. Esos límites ya


venían presentados en la tabla 2.1.

Cuando en un programa se pretende asignar a una variable un valor que


no pertenece al dominio, el resultado es habitualmente extraño. Se
suele decir que es imprevisible, pero la verdad es que la electrónica del
procesador actúa de la forma para la que está diseñada, y no son
valores aleatorios los que se alcanzan entonces, aunque sí, muchas
veces, valores no deseados, o valores erróneos.

Se muestra ahora el comportamiento de las variables ante un


desbordamiento (que así se le llama) de la memoria. Si la variable es
entera, ante un desbordamiento de memoria el procesador trabaja de la
misma forma que lo hace, por ejemplo, el cuenta kilómetros de un
vehículo. Si en un cuenta kilómetros de cinco dígitos, está marcado el
valor 99.998 kilómetros, al recorrer cinco kilómetros más, el valor que
aparece en pantalla será 00.003. Se suele decir que se le ha dado la
vuelta al marcador. Y algo similar ocurre con las variables enteras. En la

48
Capítulo 2. Tipos de datos y variables en C.

tabla 2.7. se muestran los valores de diferentes operaciones con


desbordamiento.

signed short 32767 + 1 da el valor –32768


unsigned short 65535 + 1 da el valor 0
signed long 2147483647 +1 da el valor -2147483648
unsigned long 4294967295 + 1 da el valor 0

Tabla 2.7.: valores de desbordamiento en las variables de


tipo entero.

Si el desbordamiento se realiza por asignación directa, es decir,


asignando a una variable un literal que sobrepasa el rango de su
dominio, o asignándole el valor de una variable de rango superior,
entonces se almacena el valor truncado. Por ejemplo, si a una variable
unsigned short se le asigna un valor que requiere 25 dígitos binarios,
únicamente se quedan almacenados los 16 menos significativos. A eso
hay que añadirle que, si la variable es signed short, al tomar los 16
dígitos menos significativos, interpretará el más significativo de ellos
como el bit de signo, y según sea ese bit, interpretará toda la
información codificada como entero negativo en complemento a la base,
o como entero positivo.

Hay situaciones y problemas donde jugar con las reglas de


desbordamiento de enteros ofrece soluciones muy rápidas y buenas.
Pero, evidentemente, en esos casos hay que saber lo que se hace.

Si el desbordamiento se realiza con variables en coma flotante el


resultado es mucho más complejo de prever, y no resulta sencillo
pensar en una situación en la que ese desbordamiento pueda ser
deseado.

Si el desbordamiento es por asignación, la variable desbordada


almacenará un valor que nada tendrá que ver con el original. Si el
desbordamiento tiene lugar por realizar operaciones en un tipo de dato
de coma flotante y en las que el valor final es demasiado grande para

49
Fundamentos de informática. Programación en Lenguaje C

ese tipo de dato, entonces el resultado es completamente imprevisible,


y posiblemente se produzca una interrupción en la ejecución del
programa. Ese desbordamiento se considera, sin más, error de
programación.

Constantes. Directiva #define.


Cuando se desea crear una variable a la que se asigna un valor inicial
que no debe modificarse, se la precede, en su declaración, de la palabra
clave de C const.

const tipo var_1 = val_1[,var_2 = val_2, …, var_N = val_N];

Se declara con la palabra reservada const. Pueden definirse constantes


de cualquiera de los tipos de datos simples.

const float DOS_PI = 6.28;

Como no se puede modificar su valor, la constante no puede ser un


Lvalue (excepto en el momento de su inicialización). Si se intenta
modificar el valor de la constante mediante el operador asignación el
compilador dará un error y no se creará el programa.

Otro modo de definir constantes es mediante la directiva de


preprocesador o de compilación #define. Ya se ha visto en el primer
capítulo otra directiva que se emplea para indicar al compilador de qué
bibliotecas se toman funciones ya creadas y compiladas: la directiva
#include.

#define DOS_PI 6.28

Como se ve, la directiva #define no termina en punto y coma. Eso es


debido a que las directivas de compilación no son instrucciones de C,
sino órdenes que se dirigen al compilador. La directiva #define se
ejecuta previamente a la compilación, y sustituye, en todas las líneas de
código posteriores a la aparición de la directiva, cada aparición de la

50
Capítulo 2. Tipos de datos y variables en C.

primera cadena de caracteres por la segunda cadena: en el ejemplo


antes presentado, la cadena DOS_PI por el valor 6.28.

Ya se verá cómo esta directiva pude resultar muy útil en ocasiones.

Intercambio de valores de dos variables


Una operación bastante habitual en un programa es el intercambio de
valores entre dos variables. Supongamos el siguiente ejemplo:

<variable1, int, R, 10> y <variable2, int, R, 20>

Si queremos que variable1 almacene el valor de variable2 y variable2 el


de variable1, es necesario acudir a una variable auxiliar. El proceso es
así:
auxiliar = variable1;
variable1 = variable2;
variable2 = auxiliar;

Porque no podemos copiar el valor de variable2 en variable1 sin perder


con esta asignación el valor de variable1 que queríamos guardar en
variable2.

Con el operador or exclusivo se puede hacer intercambio de valores sin


acudir, para ello, a una variable auxiliar. El procedimiento es el
siguiente:
variable1 = variable1 ^ variable2;
variable2 = variable1 ^ variable2;
variable1 = variable1 ^ variable2;

que, con operadores compuestos queda de la siguiente manera:


variable1 ^= variable2;
variable2 ^= variable1;
variable1 ^= variable2;

Veamos un ejemplo para comprobar que realmente realiza el


intercambio:
short int variable1 = 3579;
short int variable2 = 2468;

51
Fundamentos de informática. Programación en Lenguaje C

en base binaria el valor de las dos variables es:


variable1 0 0 0 0 1 1 0 1 1 1 1 1 1 0 1 1
variable2 0 0 0 0 1 0 0 1 1 0 1 0 0 1 0 0
variable1 ^= variable2 0 0 0 0 0 1 0 0 0 1 0 1 1 1 1 1
variable2 ^= variable1 0 0 0 0 1 1 0 1 1 1 1 1 1 0 1 1
variable1 ^= variable2 0 0 0 0 1 0 0 1 1 0 1 0 0 1 0 0

Al final del proceso, el valor de variable1 es el que inicialmente tenía


variable2, y al revés. Basta comparar valores. Para verificar que las
operaciones están correctas (que lo están) hay que tener en cuenta que
el proceso va cambiando los valores de variable1 y de variable2, y esos
cambios hay que tenerlos en cuenta en las siguientes operaciones or
exclusivo a nivel de bit.

Ayudas On line.
Muchos editores y compiladores de C cuentan con ayudas en línea
abundante. Todo lo referido en este capítulo puede encontrarse en ellas.
Es buena práctica de programación saber manejarse por esas ayudas,
que llegan a ser muy voluminosas y que gozan de buenos índices para
lograr encontrar el auxilio necesario en cada momento.

Recapitulación.
Después de estudiar este capítulo, ya sabemos crear y operar con
nuestras variables. También conocemos muchos de los operadores
definidos en C. Con todo esto podemos realizar ya muchos programas
sencillos.

Si conocemos el rango o dominio de cada tipo de dato, sabemos


también de qué tipo conviene que sea cada variable que necesitemos. Y
estaremos vigilantes en las operaciones que se realizan con esas
variables, para no sobrepasar ese dominio e incurrir en un overflow.

También hemos visto las reglas para combinar, en una expresión,


variables y valores de diferente tipo de dato. Es importante conocer bien

52
Capítulo 2. Tipos de datos y variables en C.

todas las reglas que gobiernan estas combinaciones porque con


frecuencia, si se trabaja sin tiento, se llegan a resultados erróneos.

Con los ejercicios que se proponen a continuación se pueden practicar y


poner a prueba nuestros conceptos adquiridos.

Ejemplos y ejercicios propuestos.

1. Escribir un programa que realice las operaciones de suma,


resta, producto, cociente y módulo de dos enteros introducidos
por teclado.

#include <stdio.h>
void main(void)
{
signed long a, b;
signed long sum, res, pro, coc, mod;

printf("Introduzca el valor del 1er. operando ... ");


scanf("%ld",&a);
printf("Introduzca el valor del 2do. operando ... ");
scanf("%ld",&b);

// Cálculos
sum = a + b;
res = a - b;
pro = a * b;
coc = a / b;
mod = a % b;

// Mostrar resultados por pantalla.


printf("La suma es igual a %ld\n", sum);
printf("La resta es igual a %ld\n", res);
printf("El producto es igual a %ld\n", pro);
printf("El cociente es igual a %ld\n", coc);
printf("El resto es igual a %ld\n", mod);
}

53
Fundamentos de informática. Programación en Lenguaje C

Observación: cuando se realiza una operación de cociente o de resto


es muy recomendable antes verificar que, efectivamente, el divisor no
es igual a cero. Aún no sabemos hacerlo, así que queda el programa un
poco cojo. Al ejecutarlo será importante que el usuario no introduzca un
valor para la variable b igual a cero.

2. Repetir el mismo programa para números de coma flotante.

#include <stdio.h>
void main(void)
{
float a, b;
float sum, res, pro, coc;

printf("Introduzca el valor del 1er. operando ... ");


scanf("%f",&a);
printf("Introduzca el valor del 2do. operando ... ");
scanf("%f",&b);

// Cálculos
sum = a + b;
res = a - b;
pro = a * b;
coc = a / b;
// mod = a % b; : esta operación no está permitida
// Mostrar resultados por pantalla.
printf("La suma es igual a %f\n", sum);
printf("La resta es igual a %f\n", res);
printf("El producto es igual a %f\n", pro);
printf("El cociente es igual a %f\n", coc);
}

En este caso se ha tenido que omitir la operación módulo, que no está


definida para valores del dominio de los números de coma flotante. Al
igual que en el ejemplo anterior, se debería verificar (aún no se han
presentado las herramientas que lo permiten), antes de realizar el
cociente, que el divisor era diferente de cero.

54
Capítulo 2. Tipos de datos y variables en C.

3. Escriba un programa que resuelva una ecuación de primer


grado ( a ⋅ x + b = 0 ). El programa solicita los valores de los
parámetros a y b y mostrará el valor resultante de la variable x.

#include <stdio.h>
void main(void)
{
float a, b;
printf("Introduzca el valor del parámetro a ... ");
scanf("%f",&a);
printf("Introduzca el valor del parámetro b ... ");
scanf("%f",&b);
printf("x = %f",-b / a);
}

De nuevo sería más correcto el programa si, antes de realizar el


cociente, se verificase que la variable a es distinta de cero.

4. Escriba un programa que solicite un entero y muestre por


pantalla su valor al cuadrado y su valor al cubo.

#include <stdio.h>
void main(void)
{
short x;
long cuadrado, cubo;
printf("Introduzca un valor ... ");
scanf("%hi",&x);
cuadrado = (long)x * x;
cubo = cuadrado * x;
printf("El cuadrado de %hd es %li\n",x, cuadrado);
printf("El cubo de %hd es %li\n",x, cubo);
}

Es importante crear una presentación cómoda para el usuario. No


tendría sentido comenzar el programa por la función scanf, porque en
ese caso el programa comenzaría esperando un dato del usuario, sin
aviso previo que le indicase qué es lo que debe hacer. En el siguiente

55
Fundamentos de informática. Programación en Lenguaje C

tema se presenta con detalle las dos funciones de entrada y salida por
consola.

La variable x es short. Al calcular el valor de la variable cuadrado


forzamos el tipo de dato para que el valor calculado sea long y no se
pierda información en la operación. En el cálculo del valor de la variable
cubo no es preciso hacer esa conversión, porque ya la variable cuadrado
es de tipo long. En esta última operación no queda garantizado que no
se llegue a un desbordamiento: cualquier valor de x mayor de 1290
tiene un cubo no codificable con 32 bits. Se puede probar qué ocurre
introduciendo valores mayores que éste indicado.

5. Escriba un programa que solicite los valores de la base y de la


altura de un triángulo y que imprima por pantalla el valor de la
superficie.

#include <stdio.h>
void main(void)
{
double b, h, S;
printf("Introduzca la base ... ");
scanf("%lf",&b);
printf("Introduzca la altura ... ");
scanf("%lf",&h);
S = b * h / 2;
printf("La superficie del triangulo de ");
printf("base %.2lf y altura %.2lf ",b,h);
printf("es %.2lf",S);
}

Las variables se han tomado double. Así no se pierde información en la


operación cociente. Puede probar a declarar las variables como de tipo
short, modificando también algunos parámetros de las funciones de
entrada y salida por consola:
void main(void)
{
short b, h, S;

56
Capítulo 2. Tipos de datos y variables en C.

printf("Introduzca la base ... ");


scanf("%hd",&b);
printf("Introduzca la altura ... ");
scanf("%hd",&h);
S = b * h / 2;
printf("La superficie del triangulo de ");
printf("base %hd y altura %hd ",b,h);
printf("es %hd",S);
}

Si al ejecutar el programa el usuario introduce los valores 5 para la base


y 3 para la altura, el valor de la superficie que mostrará el programa al
ejecutarse será ahora de 7, y no de 7,5.

6. Escriba un programa que solicite el valor del radio y muestre la


longitud de su circunferencia y la superficie del círculo inscrito
en ella.

#include <stdio.h>
#define PI 3.14159
void main(void)
{
signed short int r;
double l, S;
const double pi = 3.14159;
printf("Indique el valor del radio ... ");
scanf("%hd",&r);
printf("La longitud de la circunferencia");
printf(" cuyo radio es %hd",r);
l = 2 * pi * r;
printf(" es %lf. \n",l);
printf("La superficie de la circunferencia");
printf(" cuyo radio es %hd",r);
S = PI * r * r;
printf(" es %lf. \n",S);
}

En este ejemplo hemos mezclado tipos de dato. El radio lo tomamos


como entero. Luego, en el cálculo de la longitud l, como la expresión
tiene el valor de la constante pi, que es double, se produce una
conversión implícita de tipo de dato, y el resultado final es double.

57
Fundamentos de informática. Programación en Lenguaje C

Para el cálculo de la superficie, en lugar de emplear la constante pi se ha


tomado un identificador PI definido mediante la directiva #define. El
valor de PI también tiene forma de coma flotante, y el resultado será
por tanto de este tipo.

A diferencia de los ejemplos anteriores, en éste hemos guardado los


resultados obtenidos en variables.

7. Escriba un programa que solicite el valor de la temperatura en


grados Fahrenheit y muestre por pantalla el equivalente en
grados Celsius. La ecuación que define esta trasformación es:

Celsius = (5 / 9) · (Fahrenheit – 32).

#include <stdio.h>
void main(void)
{
double fahr, cels;
printf("Temperatura en grados Fahrenheit ... ");
scanf("%lf",&fahr);
cels = (5 / 9) * (fahr - 32);
printf("La temperatura en grados Celsius ");
printf("resulta ... %lf.",cels);
}

Tal y como está escrito el código parece que todo ha de ir bien. Si


ensayamos el programa con la entrada en grado Fahrenheit igual a 32,
entonces la temperatura en Celsius resulta 0, que es correcto puesto
que esas son las temperaturas, en las dos tablas, en las que se derrite
el hielo.

Pero, ¿y si probamos con otra entrada?... ¡También da 0! ¿Por qué?

Pues porque (5 / 9) es una operación cociente entre dos enteros, cuyo


resultado es un entero: el truncado, es decir, el mayor entero menor
que el resultado de la operación; es decir, 0.

¿Cómo se debería escribir la operación?

58
Capítulo 2. Tipos de datos y variables en C.

cels = (5 / 9.0) * (fahr - 32);


cels = (5.0 / 9) * (fahr - 32);
cels = ((double)5 / 9) * (fahr - 32);
cels = (5 / (double)9) * (fahr - 32);
cels = 5 * (fahr - 32) / 9;

Hay muchas formas: lo importante es saber en todo momento qué


operación realizará la sentencia que escribimos. Hay que saber escribir
expresiones que relacionan valores de distinto tipo para no perder nunca
información por falta de rango al elegir mal uno de los tipos de alguno
de los literales o variables.

Es importante, en este tema de los lenguajes, no perder de vista la


realidad física que subyace en toda la programación. Un lenguaje de
programación no es una herramienta que relacione variables en el
sentido matemático de la palabra. Relaciona elementos físicos, llamados
posiciones de memoria. Y es importante, al programar, razonar de
acuerdo con la realidad física con la que trabajamos.

8. Rotaciones de bits dentro de un entero.

Una operación que tiene uso en algunas aplicaciones de tipo


criptográficos es la de rotación de un entero. Rotar un número x
posiciones hacia la derecha consiste en desplazar todos sus bits hacia la
derecha esas x posiciones, pero sin perder los x bits menos
significativos, que pasarán a situarse en la parte más significativa del
número. Por ejemplo, el número a = 1101 0101 0011 1110 rotado 4 bits
a la derecha queda 1110 1101 0101 0011, donde los cuatro bits menos
significativos (1110) se han venido a posicionar en la izquierda del
número.

59
Fundamentos de informática. Programación en Lenguaje C

La rotación hacia la izquierda es análogamente igual, donde ahora los x


bits más significativos del número pasan a la derecha del número. El
número a rotado 5 bits a la izquierda quedaría: 1010 0111 1101 1010.

Qué órdenes deberíamos dar para lograr la rotación de números


a la izquierda:

unsigned short int a, b, despl;


a = 0xABCD;
despl = 5;
b = ((a << despl) | (a >> (8 * sizeof(short) - despl)));

Veamos cómo funciona este código:


a: 1010 1011 1100 1101
despl: 5
a << despl: 0111 1001 1010 0000 Å
8 * sizeof(short) – despl: 8 * 2 – 5 = 11
a >> 11: 0000 0000 0001 0101 Å
b: 0111 1001 1011 0101

Y para lograr la rotación de números a la derecha:


unsigned short int a, b, despl;
a = 0xABCD;
despl = 5;
b = ((a >> despl) | (a << (8 * sizeof(short) - despl)));

9. Indicar el valor que toman las variables en las siguientes


asignaciones.

int a = 10, b = 5, c, d;
float x = 10.0 ,y = 5.0, z, t, v;
c = a/b;
d = x/y;
z = a/b;
t = (1 / 2) * x;
v = (1.0 / 2) * x;

60
Capítulo 2. Tipos de datos y variables en C.

10. Escribir el siguiente programa y justificar la salida que ofrece


por pantalla.

#include <stdio.h>
void main(void)
{
char a = 127;
a++;
printf("%hd", a);
}

La salida que se obtiene con este código es… -128. Intente justificar por
qué ocurre. No se preocupe si aún no conoce el funcionamiento de la
función printf(). Verdaderamente la variable a ahora vale -128. ¿Por
qué?

Si el código que se ejecuta es el siguiente, explique la salida obtenida.


¿Sabría adivinar qué va a salir por pantalla antes de ejecutar el
programa?
#include <stdio.h>
void main(void)
{
short sh = 0x7FFF;
long ln = 0x7FFFFFFF;
printf("\nEl valor inicial de sh es ... %hd", sh);
printf("\nEl valor inicial de ln es ... %ld", ln);
sh++;
ln++;
printf("\nEl valor final de sh es ... %hd", sh);
printf("\nEl valor final de ln es ... %ld", ln);
}

11. Escribir un programa que indique cuántos bits y bytes ocupa


una variable long.

#include <stdio.h>
void main(void)
{

61
Fundamentos de informática. Programación en Lenguaje C

short bits, bytes;


bytes = sizeof(long);
bits = 8 * bytes;
printf("BITS = %hd - BYTES = %hd.", bits, bytes);
}

12. Escribir el siguiente programa y explique las salidas que ofrece


por pantalla.

#include <stdio.h>

void main(void)
{
char a = 'X', b = 'Y';
printf("\nvalor (caracter) de a: %c", a);
printf("\nvalor (caracter) de b: %c", b);

printf("\nvalor de a (en base 10): %hd", a);


printf("\nvalor de b (en base 10): %hd", b);

printf("\nvalor (caracter) de a + b: %c", a + b);


printf("\nvalor (en base 10) de a + b: %hd", a + b);

printf("\nvalor (caracter) de a+b + 5: %c", a + b + 5);


printf("\nvalor (en base 10) de a+b+5: %hd", a+b + 5);
}

La salida que se obtiene por pantalla es:


valor (caracter) de a: X
valor (caracter) de b: Y
valor de a (en base 10): 88
valor de b (en base 10): 89
valor (caracter) de a + b: ▒
valor (en base 10) de a + b: 177
valor (caracter) de a + b + 5: Â
valor (en base 10) de a + b + 5: 182

13. Escriba un programa que calcule la superficie y el volumen de


una esfera cuyo radio es introducido por teclado.

S = 4 ⋅ π ⋅ r2 V = 4 3 ⋅ π ⋅ r3

62
Capítulo 2. Tipos de datos y variables en C.

(Recuerde que, en C, la expresión 4 3 no significa el valor racional


resultante del cociente, sino un nuevo valor entero, que es
idénticamente igual a 1.)

14. El número áureo ( α ) es aquel que verifica la propiedad de que


al elevarlo al cuadrado se obtiene el mismo valor que al
sumarle 1. Haga un programa que calcule y muestre por
pantalla el número áureo.

#include <stdio.h>
#include <math.h>
void main(void)
{
double aureo;
printf("Número AUREO: tal que x + 1 = x * x.\n");
// Cálculo del número Aureo
// x^2 = x + 1 ==> x^2 - x - 1 = 0 ==> x = (1 + sqrt(5)) / 2.
aureo = (1 + sqrt(5)) / 2;
printf("El número AUREO es .. %lf\n",aureo);
printf("aureo + 1 ........... %lf\n",aureo + 1);
printf("aureo * aureo ....... %lf\n", aureo * aureo);
}

La función sqrt() está definida en la biblioteca math.h. Calcula el valor


de la raíz cuadrada de un número. Espera como parámetro una variable
de tipo double, y devuelve el valor en este formato o tipo de dato.

El ejercicio es muy sencillo. La única complicación (si se le puede llamar


complicación a esta trivialidad) es saber cómo se calcula el número
aureo a partir de la definición aportada. Muchas veces el problema de la
programación no está en el lenguaje, sino en saber expresar una
solución viable de nuestro problema.

63
Fundamentos de informática. Programación en Lenguaje C

15. Escriba un programa que calcule a qué distancia caerá un


proyectil lanzado desde un cañón. El programa recibe desde
teclado el ángulo inicial de salida del proyectil ( α ) y su
velocidad inicial ( V0 ).

Tenga en cuenta las siguientes ecuaciones que definen el


comportamiento del sistema descrito:
Vx = V0 ⋅ cos(α ) Vy = V0 ⋅ sen(α ) − g ⋅ t

1
x = V0 ⋅ t ⋅ cos(α ) y = V0 ⋅ t ⋅ sen(α ) − ⋅ g ⋅ t2
2

64
CAPÍTULO 3
FUNCIONES DE ENTRADA Y SALIDA
POR CONSOLA

Hasta el momento, hemos presentado las sentencias de creación y


declaración de variables. También hemos visto multitud de operaciones
que se pueden realizar con las diferentes variables y literales. Pero aún
no sabemos cómo mostrar un resultado por pantalla. Y tampoco hemos
aprendido todavía a introducir información, para un programa en
ejecución, desde el teclado.

El objetivo de este breve capítulo es iniciar en la comunicación entre el


programa y el usuario.

Lograr que el valor de una variable almacenada de un programa sea


mostrado por pantalla sería una tarea compleja si no fuese porque ya
ANSI C ofrece funciones que realizan esta tarea. Y lo mismo ocurre
cuando el programador quiere que sea el usuario quien teclee una
entrada durante la ejecución del programa.
Fundamentos de informática. Programación en Lenguaje C

Estas funciones, de entrada y salida estándar de datos por consola,


están declaradas en un archivo de cabecera llamado stdio.h. Siempre
que deseemos usar estas funciones deberemos añadir, al principio del
código de nuestro programa, la directriz #include <stdio.h>.

Salida de datos. La función printf().


El prototipo de la función es el siguiente:

int printf(const char *cadena_control[, argumento, ...]);

Qué es un prototipo de una función es cuestión que habrá que explicar


en otro capítulo. Sucintamente, diremos que el prototipo indica el modo
de empleo de la función: qué tipo de dato devuelve y qué valores espera
recibir cuando se hace uso de ella. El prototipo nos sirve para ver cómo
se emplea esta función.

La función printf devuelve un valor entero. Se dice que es de tipo int. La


función printf devuelve un entero que indica el número de bytes que ha
impreso en pantalla. Si, por la causa que sea, la función no se ejecuta
correctamente, en lugar de ese valor entero lo que devuelve es un valor
que significa error (por ejemplo un valor negativo). No descendemos a
más detalles.

La función, como toda función, lleva después del nombre un par de


paréntesis. Entre ellos va redactado el texto que deseamos que quede
impreso en la pantalla. La cadena_control indica el texto que debe ser
impreso, con unas especificaciones que indican el formato de esa
impresión; es una cadena de caracteres recogidos entre comillas, que
indica el texto que se ha de mostrar por pantalla. A lo largo de este
capítulo aprenderemos a crear esas cadenas de control que especifican
la salida y el formato que ha de mostrar la función printf.

Para comenzar a practicar, comenzamos por escribir en el editor de C el


siguiente código. Es muy recomendable que a la hora de estudiar

66
Capítulo 3. Funciones de entrada y salida por consola.

cualquier lenguaje de programación, y ahora en concreto el lenguaje C,


se trabaje delante de un ordenador que tenga un editor y un compilador
de código:
#include <stdio.h>
void main(void)
{
printf(“Texto a mostrar en pantalla”);
}

Que ofrecerá la siguiente salida por pantalla

Texto a mostrar en pantalla

Y así, cualquier texto que se escriba entre las comillas aparecerá en


pantalla.

Si introducimos ahora otra instrucción con la función printf a


continuación y debajo de la otra, por ejemplo

printf(“Otro texto”);

Entonces lo que tendremos en pantalla será

Texto a mostrar en pantallaOtro texto

Y es que la función printf continua escribiendo donde se quedó la vez


anterior.

Muchas veces nos va a interesar introducir, en nuestra cadena de


caracteres que queremos imprimir por pantalla, algún carácter de, por
ejemplo, salto de línea. Pero si tecleamos la tecla intro en el editor de C
lo que hace el cursor en el editor es cambiar de línea y eso que no
queda reflejado luego en el texto que muestra el programa en tiempo de
ejecución.

Para poder escribir este carácter de salto de línea, y otros que llamamos
caracteres de control, se escribe, en el lugar de la cadena donde
queremos que se imprima ese carácter especial, una barra invertida (‘\’)
seguida de una letra. Cuál letra es la que se debe poner dependerá de
qué carácter especial se desea introducir. Esos caracteres de control son

67
Fundamentos de informática. Programación en Lenguaje C

caracteres no imprimibles, o caracteres que tienen ya un significado


especial en la función printf.

Por ejemplo, el código anterior quedaría mejor de la siguiente forma:


#include <stdio.h>
void main(void)
{
printf(“Texto a mostrar en pantalla\n”);
printf(“Otro texto.”)
}

que ofrecerá la siguiente salida por consola:


Texto a mostrar en pantalla
Otro texto

Ya que al final de la cadena del primer printf hemos introducido un


carácter de control salto de línea: “\n” significa, dentro de la función
printf, salto de línea.

Las demás letras con significado para un carácter de control en esta


función vienen recogidas en la tabla 3.1.

\a Carácter sonido. Emite un pitido breve.


\v Tabulador vertical.
\0 Carácter nulo.
\n Nueva línea.
\t Tabulador horizontal.
\b Retroceder un carácter.
\r Retorno de carro.
\f Salto de página.
\’ Imprime la comilla simple.
\” Imprime la comilla doble.
\\ Imprime la barra invertida ‘\’.
\xdd dd es el código ASCII, en hexadecimal, del
carácter que se desea imprimir.

Tabla 3.1.: Caracteres de control en la cadena de


la función printf.

Muchas pruebas se pueden hacer ya en el editor de C, para compilar y


ver la ejecución que resulta. Gracias a la última opción de la tabla 3.1.
es posible imprimir todos los caracteres ASCII y los tres inmediatamente

68
Capítulo 3. Funciones de entrada y salida por consola.

anteriores sirven para imprimir caracteres que tienen un significado


preciso dentro de la cadena de la función printf. Gracias a ellos podemos
imprimir, por ejemplo, un carácter de comillas dobles evitando que la
función printf interprete ese carácter como final de la cadena que se
debe imprimir.

El siguiente paso, una vez visto cómo imprimir texto prefijado, es


imprimir en consola el valor de una variable de nuestro programa.

Cuando en un texto a imprimir se desea intercalar el valor de una


variable, en la posición donde debería ir ese valor se coloca el carácter
‘%’ seguido de algunos caracteres. Según los caracteres que se
introduzcan, se imprimirá un valor de un tipo de dato concreto, con un
formato de presentación determinado. Ese carácter ‘%’ y esos
caracteres que le sigan son los especificadores de formato. Al final de
la cadena, después de las comillas de cierre, se coloca una coma y el
nombre de la variable que se desea imprimir.

Por ejemplo, el siguiente código


#include <stdio.h>
void main(void)
{
short int a = 5 , b = 10 , c;
c = a + b++;
printf(“Ahora c vale %hd \n”,c);
printf(“y b vale ahora %hd ”,b);
}

Que ofrece la siguiente salida por pantalla:


Ahora c vale 15
y b vale ahora 11

Una cadena de texto de la función printf puede tener tantos


especificadores de formato como se desee. Tantos como valores de
variables queramos imprimir por pantalla. Al final de la cadena, y
después de una coma, se incluyen tantas variables, separadas también
por comas, como especificadores de formato se hayan incluido en la
cadena de texto. Cada grupo de caracteres encabezado por % en el

69
Fundamentos de informática. Programación en Lenguaje C

primer argumento de la función (la cadena de control) está asociado con


el correspondiente segundo, tercero, etc. argumento de esa función.
Debe existir una correspondencia biunívoca entre el número de
especificadores de formato y el número de variables que se recogen
después de la cadena de control; de lo contrario se obtendrán resultados
imprevisibles y sin sentido.

El especificador de formato instruye a la función sobre la forma en que


deben ir impresos cada uno de los valores de las variables que
deseamos que se muestren por pantalla.

Los especificadores tienen la forma:

%[flags][ancho campo][.precisión][F/N/h/l/L] type

Veamos los diferentes componentes del especificador de formato:

type: Es el único argumento necesario. Consiste en una letra que indica


el tipo de dato a que corresponde al valor que se desea imprimir en
esa posición. En la tabla 3.2. se recogen todos los valores que definen
tipos de dato. Esta tabla está accesible en las ayudas de editores y
compiladores de C.

• [F / N / h / l / L]: Estas cinco letras son modificadores de tipo y


preceden a las letras que indican el tipo de dato que se debe mostrar
por pantalla.

La letra h es el modificador short para valores enteros.

La letra l tiene dos significados: es el modificador long para valores


enteros. Y, precediendo a la letra f indica que allí debe ir un valor de tipo
double.

La letra L precediendo a la letra f indica que allí debe ir un valor de tipo


long double.

• [ancho campo][.precisión]: Estos dos indicadores opcionales deben ir


antes de los indicadores del tipo de dato. Con el ancho de campo, el

70
Capítulo 3. Funciones de entrada y salida por consola.

especificador de formato indica a la función printf la longitud mínima


que debe ocupar la impresión del valor que allí se debe mostrar.

%d Entero con signo, en base decimal.


%i Entero con signo, en base decimal.
%o Entero (con o sin signo) codificado en base
octal.
%u Entero sin signo, en base decimal.
%x Entero (con o sin signo) codificado en base
hexadecimal, usando letras minúsculas.
Codificación interna de los enteros.
%X Entero (con o sin signo) codificado en base
hexadecimal, usando letras mayúsculas.
Codificación interna de los enteros.
%f Número real con signo.
%e Número real con signo en formato científico,
con el exponente ‘e’ en minúscula.
%E Número real con signo en formato científico,
con el exponente ‘e’ en mayúscula.
%g Número real con signo, a elegir entre formato e
ó f según cuál sea el tamaño más corto.
%G Número real con signo, a elegir entre formato E
ó f según cuál sea el tamaño más corto.
%c Un carácter. El carácter cuyo ASCII corresponda
con el valor a imprimir.
%s Cadena de caracteres.
%p Dirección de memoria.
%n No lo explicamos aquí ahora.
%% Si el carácter % no va seguido de nada,
entonces se imprime el carácter sin más.

Tabla 3.2.: Especificadores de tipo de dato en la


función printf.

Para mostrar información por pantalla la función printf emplea un tipo


de letra de paso fijo. Esto quiere decir que cada carácter impreso
ocasiona el mismo desplazamiento del cursor hacia la derecha. Al decir
que el ancho de campo indica la longitud mínima se quiere decir que
este parámetro señala cuántos avances de cursor deben realizarse,
como mínimo, al imprimir el valor.

Por ejemplo, las instrucciones

71
Fundamentos de informática. Programación en Lenguaje C

long int a = 123, b = 4567, c = 135790;


printf(“La variable a vale ... %6li.\n”,a);
printf(“La variable b vale ... %6li.\n”,b);
printf(“La variable c vale ... %6li.\n”,c);

tiene la siguiente salida:


La variable a vale ... 123.
La variable b vale ... 4567.
La variable c vale ... 135790.

donde vemos que los tres valores impresos en líneas diferentes quedan
alineados en sus unidades, decenas, centenas, etc. gracias a que todos
esos valores se han impreso con un ancho de campo igual a 6: su
impresión ha ocasionado tantos desplazamientos de cursos como indica
el ancho de campo.

Si la cadena o número es mayor que el ancho de campo indicado


ignorará el formato y se emplean tantos pasos de cursor como sean
necesarios para imprimir correctamente el valor.

Si se desea, es posible rellenar con ceros los huecos del avance de


cursor. Para ellos se coloca un 0 antes del número que indica el ancho
de campo

La instrucción

printf(“La variable a vale ... %06li.\n”,a);

ofrece como salida la siguiente línea en pantalla:

La variable a vale ... 000123.

El parámetro de precisión se emplea para valores con coma flotante.


Indica el número de decimales que se deben mostrar. Indica cuántos
dígitos no enteros se deben imprimir: las posiciones decimales. A ese
valor le precede un punto. Si el número de decimales del dato
almacenado en la variable es menor que la precisión señalada, entonces
la función printf completa con ceros ese valor. Si el número de
decimales del dato es mayor que el que se indica en el parámetro de
precisión, entonces la función printf trunca el número.

72
Capítulo 3. Funciones de entrada y salida por consola.

Por ejemplo, el código


double raiz_2 = sqrt(2);
printf("A. Raiz de dos vale %lf\n",raiz_2);
printf("B. Raiz de dos vale %12.1lf\n",raiz_2);
printf("C. Raiz de dos vale %12.3lf\n",raiz_2);
printf("D. Raiz de dos vale %12.5lf\n",raiz_2);
printf("E. Raiz de dos vale %12.7lf\n",raiz_2);
printf("F. Raiz de dos vale %12.9lf\n",raiz_2);
printf("G. Raiz de dos vale %12.11lf\n",raiz_2);
printf("H. Raiz de dos vale %5.7lf\n",raiz_2);
printf("I. Raiz de dos vale %012.4lf\n",raiz_2);

que ofrece la siguiente salida por pantalla:


A. Raiz de dos vale 1.414214
B. Raiz de dos vale 1.4
C. Raiz de dos vale 1.414
D. Raiz de dos vale 1.41421
E. Raiz de dos vale 1.4142136
F. Raiz de dos vale 1.414213562
G. Raiz de dos vale 1.41421356237
H. Raiz de dos vale 1.4142136
I. Raiz de dos vale 0000001.4142

La función sqrt está declarada en el archivo de cabecera math.h. Esta


función devuelve la raíz cuadrada (en formato double) del valor
(también double) que ha recibido como parámetro de entrada, entre
paréntesis.

Por defecto, se toman seis decimales, sin formato alguno. Se ve en el


ejemplo el truncamiento de decimales. En el caso G., la función printf
hace caso omiso del ancho de campo pues se exige que muestre un
valor que tiene un carácter para la parte entera, otro para el punto
decimal y once para los decimales: en total 13 caracteres, y no 12 como
señala en ancho de campo. y es que el punto decimal es un carácter
más dentro de la impresión por pantalla del valor.

• [flags]: Son caracteres que introducen unas últimas modificaciones en


el modo en que se presenta el valor. Algunos de sus valores y
significados son:

carácter ‘–‘: el valor queda justificado hacia la izquierda.

73
Fundamentos de informática. Programación en Lenguaje C

carácter +: el valor se escribe con signo, sea éste positivo o negativo.


En ausencia de esta bandera, la función printf imprime el signo
únicamente si es negativo.

carácter en blanco: Si el valor numérico es positivo, deja un espacio en


blanco. Si es negativo imprime el signo.

Existen otras muchas funciones que muestran información por pantalla.


Muchas de ellas están definidas en el archivo de cabecera stdio.h. Con
la ayuda a mano, es sencillo aprender a utilizar muchas de ellas.

Entrada de datos. La función scanf().


La función scanf de nuevo la encontramos declarada en el archivo de
cabecera stdio.h. Permite la entrada de datos desde el teclado. La
ejecución del programa queda suspendida en espera de que el usuario
introduzca un valor y pulse la tecla de validación (intro).

La ayuda de cualquier editor y compilador de C es suficiente para lograr


hacer un buen uso de ella. Presentamos aquí unas nociones básicas,
suficientes para su uso más habitual. Para la entrada de datos, al igual
que ocurría con la salida, hay otras funciones válidas que también
pueden conocerse a través de las ayudas de los distintos editores y
compiladores de C.

El prototipo de la función es:

int scanf(const char *cadena_control[,direcciones,…]);

La función scanf puede leer del teclado tantas entradas como se le


indiquen. De todas formas, se recomienda usar una función scanf para
cada entrada distinta que se requiera.

El valor que devuelve la función es el del número de entradas diferentes


que ha recibido. Si la función ha sufrido algún error, entonces devuelve
un valor que significa error (por ejemplo, un valor negativo).

74
Capítulo 3. Funciones de entrada y salida por consola.

En la cadena de control se indica el tipo de dato del valor que se espera


recibir por teclado. No hay que escribir texto alguno en la cadena de
control de la función scanf: únicamente el especificador de formato.

El formato de este especificador es similar al presentado en la función


printf: un carácter % seguido de una o dos letras que indican el tipo de
dato que se espera. Luego, a continuación de la cadena de control, y
después de una coma, se debe indicar dónde se debe almacenar ese
valor: la posición de una variable que debe ser del mismo tipo que el
indicado en el especificador. El comportamiento de la función scanf es
imprevisible cuando no coinciden el tipo señalado en el especificador y el
tipo de la variable; en ese caso, habitualmente aborta la ejecución del
programa.

Las letras que indican el tipo de dato a recibir se recogen en la tabla 3.3.
Los modificadores de tipo de dato son los mismos que para la función
printf.

%d Entero con signo, en base decimal.


%i Entero con signo, en base decimal.
%o Entero codificado en base octal.
%u Entero sin signo, en base decimal.
%x Entero codificado en base hexadecimal, usando
letras minúsculas. Codificación interna de los
enteros.
%X Entero codificado en base hexadecimal, usando
letras mayúsculas. Codificación interna de los
enteros.
%f Número real con signo.
%e Número real con signo en formato científico,
con el exponente ‘e’ en minúscula.
%c Un carácter. El carácter cuyo ASCII corresponda
con el valor a imprimir.
%s Cadena de caracteres.
%p Dirección de memoria.
%n No lo explicamos aquí ahora.

Tabla 3.3.: Especificadores de tipo de dato en la


función scanf.

75
Fundamentos de informática. Programación en Lenguaje C

La cadena de control tiene otras especificaciones, pero no las vamos a


ver aquí. Se pueden obtener el la ayuda del compilador.

Además de la cadena de control, la función scanf requiere de otro


parámetro: el lugar dónde se debe almacenar el valor introducido. La
función scanf espera, como segundo parámetro, el lugar donde
se aloja la variable, no el nombre. Espera la dirección de la variable.
Así está indicado en su prototipo.

Para poder saber la dirección de una variable, C dispone de un operador


unario: &. El operador dirección, prefijo a una variable, devuelve la
dirección de memoria de esta variable. El olvido de este operador en la
función scanf es frecuente en programadores noveles. Y de
consecuencias desastrosas: siempre ocurre que el dato introducido no
se almacena en la variable que deseábamos, alguna vez producirá
alteraciones de otros valores y las más de las veces llevará a la
inestabilidad del sistema y se deberá finalizar la ejecución del programa.

Recapitulación.
Hemos presentado el uso de las funciones printf() y scanf(), ambas
declaradas en el archivo de cabecera stdio.h. Cuando queramos hacer
uno de una de las dos funciones, o de ambas, deberemos indicarle al
programa con la directiva de preprocesador #include <stdio.h>.

El uso de ambas funciones se aprende en su uso habitual. Los ejercicios


del capítulo anterior pueden ayudar, ahora que ya las hemos
presentado, a practicar con ellas.

76
Capítulo 3. Funciones de entrada y salida por consola.

Ejercicios.

16. Escribir un programa que muestre al código ASCII de un


carácter introducido por teclado.

#include <stdio.h>

void main(void)
{
unsigned char ch;

printf("Introduzca un carácter por teclado ... ");


scanf("%c",&ch);

printf("El carácter introducido ha sido %c\n",ch);


printf("Su código ASCII es el %hd", ch);
}

Primero mostramos el carácter introducido con el especificador de tipo


%c: así muestra el carácter por pantalla. Y luego mostramos el mismo
valor de la variable ch con el especificador %hd, es decir, como entero
corto, y entonces nos muestra el valor numérico de ese carácter.

17. Lea el programa siguiente, e intente explicar la salida que


ofrece por pantalla.

#include <stdio.h>

void main(void)
{
signed long int sli;
signed short int ssi;

printf("Introduzca un valor negativo para sli ... ");


scanf("%ld",&sli);

printf("Introduzca un valor negativo para ssi ... ");


scanf("%hd",&ssi);

77
Fundamentos de informática. Programación en Lenguaje C

printf("El valor sli es %ld\n",sli);


printf("El valor ssi es %ld\n\n",ssi);

printf("El valor sli como \"%%lX\" es %lX\n",sli);


printf("El valor ssi como \"%%hX\" es %hX\n\n",ssi);

printf("El valor sli como \"%%lu\" es %lu\n",sli);


printf("El valor ssi como \"%%hu\" es %hu\n\n",ssi);

printf("El valor sli como \"%%hu\" es %hu\n",sli);


printf("El valor ssi como \"%%lu\" es %lu\n\n",ssi);

printf("El valor sli como \"%%hi\" es %hi\n",sli);


printf("El valor ssi como \"%%li\" es %li\n\n",ssi);
}

La salida que ha obtenido su ejecución es la siguiente:


Introduzca un valor negativo para sli ... -23564715
Introduzca un valor negativo para ssi ... -8942

El valor sli es -23564715


El valor ssi es -8942

El valor sli como "%lX" es FE986E55


El valor ssi como "%hX" es DD12

El valor sli como "%lu" es 4271402581


El valor ssi como "%hu" es 56594

El valor sli como "%hu" es 28245


El valor ssi como "%lu" es 4294958354

El valor sli como "%hi" es 28245


El valor ssi como "%li" es -8942

Las dos primeras líneas no requieren explicación alguna: recogen las


entradas que se han introducido por teclado cuando se han ejecutado
las instrucciones de la función scanf(). Las dos siguientes tampoco:
muestran por pantalla lo valores introducidos: el primero (sli) es long
int, y se muestra con el especificador de formato %ld ó %li; el segundo
(ssi) es short int, y se muestra con el especificador de formato %hd ó
%hi.

Las siguientes líneas de salida son:


El valor sli como "%lX" es FE986E55

78
Capítulo 3. Funciones de entrada y salida por consola.

El valor ssi como "%hX" es DD12

Que muestran los números tal y como los tiene codificados el


ordenador: al ser enteros con signo, y ser negativos, codifica el bit más
significativo (el bit 31 en el caso de sli, el bit 15 en el caso de ssi) a uno
porque es el bit del signo; y codifica el resto de los bits (desde el bit 30
al bit 0 en el caso de sli, desde el bit 14 hasta el bit 0 en el caso de ssi)
como el complemento a la base del valor absoluto del número
codificado.

El número (8942)10 = (22EE)16 se codifica, como número negativo (dígito


15 a 1 y resto el valor en su complemento a la base), de la forma
DD12 . Y el número (23564715)10 = (167 91AB)16 se codifica, como
número negativo, de la forma FE98 6E55 .

Las dos siguientes líneas son:


El valor sli como "%lu" es 4271402581
El valor ssi como "%hu" es 56594

Muestra el contenido de la variable lsi que considera ahora como entero


largo sin signo. Y por tanto toma esos 32 bits, que ya no los considera
como un bit de signo y 31 de complemento a la base del número
negativo, sino 32 bits de valor positivo codificado en binario:
(FE98 6E55)16 = (4.271.402.581)10 .

Y muestra el contenido de la variable ssi que considera ahora como


entero corto sin signo. Y por tanto toma esos 16 bits, que ya no los
considera como un bit de signo y 15 de complemento a la base del
número negativo, sino 16 bits de valor positivo codificado en binario:
(DD12)16 = (56.594)10 .

Las dos siguientes líneas son:


El valor sli como "%hu" es 28245
El valor ssi como "%lu" es 4294958354

La primera de ellas considera la variable sli como una variable corta de


16 bits. Por tanto lo que hace es tomar los 16 bits menos significativos

79
Fundamentos de informática. Programación en Lenguaje C

de la variable de 32 bits y los interpreta como entero corto sin signo:


(6E55)16 = (28.245)10 .

La segunda línea es de difícil interpretación: en realidad muestra un


valor numérico que, expresado en base hexadecimal, es igual a
(FFFF DD12)16 : ha añadido, delante de la variable de 16 bits, otros 16
bits que ha encontrado, casualmente, codificados en todos los bits a
uno.

El último bloque es fácilmente interpretable una vez explicadas las dos


líneas anteriores. Se deja al lector esa interpretación.

18. Escriba el siguiente programa y compruebe cómo es la salida


que ofrece por pantalla.

#include <stdio.h>
#include <math.h>
void main(void)
{
double a = M_PI;

printf(" 1. El valor de Pi es ... %20.1lf\n",a);


printf(" 2. El valor de Pi es ... %20.2lf\n",a);
printf(" 3. El valor de Pi es ... %20.3lf\n",a);
printf(" 4. El valor de Pi es ... %20.4lf\n",a);
printf(" 5. El valor de Pi es ... %20.5lf\n",a);
printf(" 6. El valor de Pi es ... %20.6lf\n",a);
printf(" 7. El valor de Pi es ... %20.7lf\n",a);
printf(" 8. El valor de Pi es ... %20.8lf\n",a);
printf(" 9. El valor de Pi es ... %20.9lf\n",a);
printf("10. El valor de Pi es ... %20.10lf\n",a);
printf("11. El valor de Pi es ... %20.11lf\n",a);
printf("12. El valor de Pi es ... %20.12lf\n",a);
printf("13. El valor de Pi es ... %20.13lf\n",a);
printf("14. El valor de Pi es ... %20.14lf\n",a);
printf("15. El valor de Pi es ... %20.15lf\n",a);
}

La salida que ofrece por pantalla es la siguiente:


1. El valor de Pi es ... ·················3.1
2. El valor de Pi es ... ················3.14

80
Capítulo 3. Funciones de entrada y salida por consola.

3. El valor de Pi es ... ···············3.142


4. El valor de Pi es ... ··············3.1416
5. El valor de Pi es ... ·············3.14159
6. El valor de Pi es ... ············3.141593
7. El valor de Pi es ... ···········3.1415927
8. El valor de Pi es ... ··········3.14159265
9. El valor de Pi es ... ·········3.141592654
10. El valor de Pi es ... ········3.1415926536
11. El valor de Pi es ... ·······3.14159265359
12. El valor de Pi es ... ······3.141592653590
13. El valor de Pi es ... ·····3.1415926535898
14. El valor de Pi es ... ····3.14159265358979
15. El valor de Pi es ... ···3.141592653589793

Donde hemos cambiado los espacios en blanco por puntos en la parte de


la impresión de los números. Y donde el archivo de biblioteca math.h
contiene el valor del número pi, en la constante o identificador M_PI.
Efectivamente, emplea 20 espacios de carácter de pantalla para mostrar
cada uno de los números. Y cambia la posición de la coma decimal, pues
cada línea exigimos a la función printf() que muestre un decimal más
que en la línea anterior.

19. Escriba un programa que genere 20 números de coma flotante


de forma aleatoria en un rango de valores entre 0 y 1 millón, y
los muestre, uno debajo de otro, alineados por la coma
decimal.

#include <stdio.h>
#include <stdlib.h>

void main(void)
{
randomize();

for(int i = 1 ; i <= 20 ; i++)


printf("%2d. %10.5lf\n",i,
(double)random(1000000) / random (10000));
}

81
Fundamentos de informática. Programación en Lenguaje C

La función random(n) genera un entero aleatorio entre 0 y n – 1. La


función randomize() debe invocarse siempre en aquellos programas que
luego se vaya a usar el generador de aleatorios y sirve para inicializar el
generador random(). Ambas funciones vienen declaradas en el archivo
de cabecera stdlib.h.

Aún no hemos llegado al capítulo de las estructuras de control, pero


podemos entender por ahora, viendo el código, que este programa
realice veinte veces la operación de generar dos enteros, dividirlos en
cociente de coma flotante (por eso, previo al cociente, convertimos el
dividendo en double) y mostramos los distintos resultados, uno debajo
del otro, con el especificador de tipo de dato %10.5lf. Así, si los
números generados son menores que 100.000 quedarán en línea, como
se ve en la salida que se muestra a continuación:
1. 57.21568
2. 62.41973
3. 147.16501
4. 120.04998
5. 215.02813
6. 52.82802
7. 260.75406
8. 721.83456
9. 9.85598
10. 150.42073
11. 7.11266
12. 78.85494
13. 73.41685
14. 196.21048
15. 88.43795
16. 192.40674
17. 315.92087
18. 50.11689
19. 7.16849
20. 187.26519

Donde la coma decimal queda ubicada, en todos los números, en la


misma columna. Y así también las unidades, decenas,… y decimales.

82
Capítulo 3. Funciones de entrada y salida por consola.

ANEXO: Ficha resumen de la función printf


printf <stdio.h>
int printf (formato [ , argumento , ...] );
Imprime los datos formateados a la salida estándar según
especifica el argumento formato.
formato
Es una cadena de texto que contiene el texto que se va a imprimir.
Opcionalmente puede contener etiquetas que se caracterizan por ir
precedidas del carácter %. Estas etiquetas serán luego sustituidas
cuando se ejecute la función por un valor especificado en los
argumentos.
El número de etiquetas de formato debe ser el mismo que el de los
argumentos que se indiquen. Cada etiqueta indica la ubicación donde se
debe insertar un valor en la cadena de texto, y el formato en que se
debe imprimir ese valor. Por cada etiqueta debe haber un argumento.
El formato de etiquetas sigue el siguiente prototipo:
%[flags][ancho][.precisión][modificadores]tipo
donde tipo es imprescindible.

tipo Salida Ejemplo


c Carácter a
doi Decimal entero con signo 392
u Decimal entero sin signo 7235
o Octal con signo 610
x Entero hexadecimal sin signo 7fa
X Entero hexadecimal sin signo (letras 7FA
mayúsculas)
f Decimal en punto flotante 392.65
e Notación científica (mantisa/exponente) con 3.9265e2
el carácter e
E Notación científica (mantisa/exponente) con 3.9265E2
el carácter E
g Usa el que sea más corto entre %e y %f 392.65
G Usa el que sea más corto entre %E y %f 392.65
s Cadena de caracteres sample
p Dirección apuntada por el argumento B800:0000

El resto de parámetros, esto es flags, ancho, .precisión y modificadores


son opcionales y siguen el siguiente formato:

83
Fundamentos de informática. Programación en Lenguaje C

modificador significado
h argumento se interpreta como un short int.
l argumento se interpreta como un long int cuando
precede a un entero (d, i, o, u, x, X) o double si
precede a un tipo de dato de coma flotante (f, g, G, e,
E).
L argumento se interpreta como un long double si
precede a un tipo de dato de coma flotante (f, g, G, e,
E).
ancho significado
num Especifica el número mínimo de caracteres que se
imprimen. Si el valor que se imprime es menor que este
num entonces el resultado es completado con ceros. El
valor nunca es truncado incluso si es mayor.
* El ancho no se especifica en el formato de la cadena
sino que se indica en el valor entero que precede al
argumento que tiene que formatearse.
.precisión significado
.num Para los tipos f, e, E, g, G: indica el número de dígitos
que se imprimen después del punto decimal (por defecto
es 6).
Para el tipo s: indica el número máximo de caracteres
que se imprimen. (por defecto imprime hasta encontrar
el primer carácter null).
flags significado
- Alineación a la izquierda con el ancho de campo dado
(por defecto alinea a la derecha). Este flag sólo tiene
sentido cuando se especifica el ancho de campo.
+ Obliga a anteponer un signo al resultado (+ o -) si el
tipo es con signo. (por defecto sólo el signo menos - se
imprime).
blanco Si el argumento es un valor positivo con signo, se
inserta un blanco antes del número.
0 Coloca tantos ceros a la izquierda del número como
sean necesarios para completar el ancho de campo
especificado.
# Usado con los tipos o, x o X el valor es precedido con 0,
0x o 0X respectivamente si no son cero.
Usado con e, E o f obliga a que el valor de salida
contenga el punto decimal incluso aunque sólo sigan
ceros.
Usado con g o G el resultado es el mismo que con e o E
pero los ceros sobrantes no se eliminan.

84
Capítulo 3. Funciones de entrada y salida por consola.

argumento(s)
Parámetro(s) opcional(es) que contiene(n) los datos que se insertarán
en el lugar de los % etiquetas especificados en los parámetros del
formato. Debe haber el mismo número de parámetros que de etiquetas
de formato.
Valor de retorno de printf.
Si tiene éxito representa el número total de caracteres impresos. Si hay
un error, se devuelve un número negativo.
Ejemplo.
/* printf: algunos ejemplos de formato*/
#include <stdio.h>

void main(void)
{
printf("Caracteres: %c %c \n", 'a', 65);
printf("Decimales: %d %ld\n", 1977, 650000);
printf("Precedidos de blancos: %10d \n", 1977);
printf("Precedidos de ceros: %010d \n", 1977);
printf("Formato: %d %x %o %#x %#o\n",100,100,100,100,100);
printf("float: %4.2f %+.0e %E\n", 3.1416, 3.1416, 3.1416);
printf("Ancho: %*d \n", 5, 10);
printf("%s \n", "Mi mamá me mima");
}
Y la salida:
Caracteres: a A
Decimales: 1977 650000
Precedidos con blancos: 1977
Precedidos con ceros: 0000001977
Formato: 100 64 144 0x64 0144
float: 3.14 +3e+000 3.141600E+000
Ancho: 10
Mi mamá me mima

85
Fundamentos de informática. Programación en Lenguaje C

86
CAPÍTULO 4
ESTRUCTURAS DE CONTROL

El lenguaje C pertenece a la familia de lenguajes del paradigma de la


programación estructurada. En este capítulo quedan recogidas las reglas
de la programación estructurada y, en concreto, las reglas sintácticas
que se exige en el uso del lenguaje C para el diseño de esas estructuras.
El objetivo del capítulo es aprender a crear estructuras
condicionales y estructuras de iteración. También veremos una
estructura especial que permite seleccionar un camino de ejecución de
entre varios establecidos. Y veremos las sentencias de salto que nos
permiten abandonar el bloque de sentencias iteradas por una estructura
de control.

Introducción.
Las reglas de la programación estructurada son:

1. Todo programa consiste en una serie de acciones o sentencias que


Fundamentos de informática. Programación en Lenguaje C

se ejecutan en secuencia, una detrás de otra.

2. Cualquier acción puede ser sustituida por dos o más acciones en


secuencia. Esta regla se conoce como la de apilamiento.

3. Cualquier acción puede ser sustituida por cualquier estructura de


control; y sólo se consideran tres estructuras de control: la
secuencia, la selección y la repetición. Esta regla se conoce
como regla de anidamiento. Todas las estructuras de control de la
programación estructurada tienen un solo punto de entrada y un solo
punto de salida.

4. Las reglas de apilamiento y de anidamiento pueden aplicarse tantas


veces como se desee y en cualquier orden.

Ya hemos visto cómo se crea una sentencia: con un punto y coma


precedido de una expresión que puede ser una asignación, la llamada a
una función, una declaración de una variable, etc. O, si la sentencia es
compuesta, agrupando entonces varias sentencias simples en un bloque
encerrado por llaves.

Los programas discurren, de instrucción a instrucción, una detrás de


otra, en una ordenación secuencial y nunca dos al mismo tiempo, como
queda representado en la figura 4.1.

Instrucción 1 Instrucción 2 … Instrucción N

Figura 4.1.: Esquema de ordenación secuencial de sentencias.

Pero un lenguaje de programación no sólo ha de poder ejecutar las


instrucciones en orden secuencial: es necesaria la capacidad para
modificar ese orden de ejecución. Para ello están las estructuras de
control. Al acabar este capítulo, una vez conocidas las estructuras de
control, las posibilidades de resolver diferentes problemas mediante el
lenguaje de programación C se habrán multiplicado enormemente.

88
Capítulo 4. Estructuras de control.

A lo largo del capítulo iremos viendo abundantes ejemplos. Es


conveniente pararse en cada uno: comprender el código que se propone
en el manual, o lograr resolver aquellos que se dejan propuestos. En
algunos casos ofreceremos el código en C; en otros dejaremos apuntado
el modo de resolver el problema ofreciendo el pseudocódigo del
algoritmo o el flujograma. Muchos de los ejemplos que aquí se van a
resolver ya tienen planteado el flujograma o el pseudocódigo en el libro
“Fundamentos de Informática. Codificación y Algoritmia”.

Conceptos previos.
La regla 3 de la programación estructurada habla de tres estructuras de
control: la secuencia, la selección y la repetición. Nada nuevo hay ahora
que decir sobre la secuencia, que vendría esquematizada en la figura
4.1. En la figura 4.2. se esquematizan diferentes posibles estructuras de
selección; y en la figura 4.3. las dos estructuras básicas de repetición.

Las dos formas que rompen el orden secuencial de ejecución de


sentencias son:

1. Instrucción condicional: Se evalúa una condición y si se cumple


se transfiere el control a una nueva dirección indicada por la
instrucción.

2. Instrucción incondicional. Se realiza la transferencia a una nueva


dirección sin evaluar ninguna condición (por ejemplo, llamada a una
función).

En ambos casos la transferencia del control se puede realizar con o sin


retorno: en el caso de que exista retorno, después de ejecutar el bloque
de instrucciones de la nueva dirección se retorna a la dirección que
sucede a la que ha realizado el cambio de flujo.

89
Fundamentos de informática. Programación en Lenguaje C

No Sí No Sí
Condición Condición

Instrucción Instrucción Instrucción

Instrucción Instrucción

Bifurcación abierta Bifurcación cerrada

Figura 4.2.: Estructuras de selección.

Instrucción
No Sí
Condición

No Condición

Instrucción Instrucción

Instrucción

Estructura while Estructura do - while

Figura 4.3.: Estructuras de repetición.

Las estructuras de control que se van a ver en este capítulo son


aquellas con trasfieren el control a una nueva dirección, de
acuerdo a una condición evaluada.

Estructuras de control condicionales.


Las estructuras de control condicionales que se van a ver son la
bifurcación abierta y la bifurcación cerrada. Un esquema del flujo de
ambas estructuras ha quedado recogido en la Figura 4.2.

• La bifurcación abierta. La sentencia if.

La sentencia que está precedida por la estructura de control


condicionada se ejecutará si la condición de la estructura de control es
verdadera; en caso contrario no se ejecuta la instrucción condicionada y

90
Capítulo 4. Estructuras de control.

continua el programa con la siguiente instrucción. En la figura 4.2. se


puede ver un esquema del comportamiento de la bifurcación abierta.

La sintaxis de la estructura de control condicionada abierta es la


siguiente:

if(condición) sentencia;

Si la condición es verdadera (distinto de cero en el lenguaje C), se


ejecuta la sentencia. Si la condición es falsa (igual a cero en el lenguaje
C), no se ejecuta la sentencia.

Si en lugar de una sentencia, se desean condicionar varias de ellas,


entonces se crea una sentencia compuesta mediante llaves.

Ejemplo:

Programa que solicite dos valores enteros y muestre el cociente:


#include <stdio.h>
void main(void)
{
short D, d;
printf("Programa para dividir dos enteros...\n");
printf("Introduzca el dividendo ... ");
scanf("%hd",&D);
printf("Introduzca el divisor ... ");
scanf("%hd",&d);
if(d != 0) printf("%hu / %hu = %hu", D, d, D / d);
}

Se efectuará la división únicamente en el caso en que se verifique la


condición de que d != 0.

• La bifurcación cerrada. La sentencia if – else.

En una bifurcación cerrada, la sentencia que está precedida por una


estructura de control condicionada se ejecutará si la condición de la
estructura de control es verdadera; en caso contrario se ejecuta una
instrucción alternativa. Después de la ejecución de una de las dos
sentencias (nunca las dos), el programa continúa la normal ejecución de
las restantes sentencias que vengan a continuación.

91
Fundamentos de informática. Programación en Lenguaje C

La sintaxis de la estructura de control condicionada cerrada es la


siguiente:
if(condición) sentencia1;
else sentencia2;

Si la condición es verdadera (distinto de cero en el lenguaje C), se


ejecuta la sentencia llamada sentencia1. Si la condición es falsa (igual a
cero en el lenguaje C), se ejecuta la sentencia llamada sentencia2.

Si en lugar de una sentencia, se desean condicionar varias de ellas,


entonces se crea una sentencia compuesta mediante llaves.

Ejemplo:

El mismo programa anteriormente visto. Quedará mejor si se escribe de


la siguiente forma:
#include <stdio.h>
void main(void)
{
short D, d;
printf("Programa para dividir dos enteros...\n");
printf("Introduzca el dividendo ... ");
scanf("%hd",&D);
printf("Introduzca el divisor ... ");
scanf("%hd",&d);
if(d != 0) printf("%hu / %hu = %hu", D, d, D / d);
else printf(“No se puede realizar division por cero”);
}

Se efectuará la división únicamente en el caso en que se verifique la


condición de que d != 0. Si el divisor introducido es igual a cero,
entonces imprime en pantalla un mensaje de advertencia.

• Anidamiento de estructuras condicionales.

Decimos que se produce anidamiento de estructuras de control cuando


una estructura de control aparece dentro de otra estructura de control
del mismo tipo.

Tanto en la parte if como en la parte else, los anidamientos pueden


llegar a cualquier nivel. De esa forma podemos elegir entre numerosas
sentencias estableciendo las condiciones necesarias.

92
Capítulo 4. Estructuras de control.

Una estructura de anidamiento tiene, por ejemplo, la forma:

if(expresión_1) /* primer if */
{
if(expresión_2) /* segundo if */
{
if(expresión_3) /* tercer if */
sentencia_1;
else /* alternativa al tercer if */
sentencia_2;
}
else /* alternativa al 2º if */
sentencia_3;
}
else /* alternativa al primer if */
sentencia_4;

(se puede ver el organigrama de este código en la figura 4.4.)

Cada else se asocia al if más próximo en el bloque en el que se


encuentre y que no tenga asociado un else. No está permitido (no
tendría sentido) utilizar un else sin un if previo. Y la estructura else
debe ir inmediatamente después de la sentencia condicionada con su if.

Sí No
C1

Sí No
C2

Sí No
C3

S1 S2 S3 S4

Figura 4.4.: Ejemplo de


estructuras condicionales anidadas. F

Un ejemplo de estructura anidada sería, siguiendo con los ejemplos


anteriores, el caso de que, si el divisor introducido ha sido el cero, el
programa preguntase si se desea introducir un divisor distinto.

93
Fundamentos de informática. Programación en Lenguaje C

#include <stdio.h>
void main(void)
{
short D, d;
char opcion;
printf("Programa para dividir dos enteros...\n");
printf("Introduzca el dividendo ... ");
scanf("%hd",&D);
printf("Introduzca el divisor ... ");
scanf("%hd",&d);
if(d != 0)
printf("%hu / %hu = %hu", D, d, D / d);
else
{
printf("No se puede dividir por cero.\n");
printf("¿Introducir otro denominador (s/n)?");
opcion = getchar();
if(opcion == 's')
{
printf("\nNuevo denominador ... ");
scanf("%hd",&d);
if(d != 0)
printf("%hu / %hu = %hu", D, d, D/d);
else
printf("De nuevo ha introducido 0.");
}
}
}

La función getchar() está definida en la biblioteca stdio.h. Esta función


espera a que el usuario pulse una tecla del teclado y, una vez pulsada,
devuelve el código ASCII de la tecla pulsada.

En este ejemplo hemos llegado hasta un tercer nivel de anidación.

• Escala if - else – if

Cuando se debe elegir entre una lista de opciones, y únicamente una de


ellas ha de ser válida, se llega a producir una concatenación de
condiciones de la siguiente forma:
if(condición1) setencia1;
else
{
if(condición2) sentencia2;
else
{
if(condición3) sentencia3;

94
Capítulo 4. Estructuras de control.

else sentencia4;
}
}

El flujograma recogido en la Figura 4.4. representaría esta situación sin


más que intercambiar los caminos de verificación de las condiciones C1,
C2 y C3 recogidas en él (es decir, intercambiando los rótulos de “Sí” y
de “No”).

Este tipo de anidamiento se resuelve en C con la estructura else if, que


permite una concatenación de las condicionales. Un código como el
antes escrito quedaría:
if(condición1) sentencia1;
else if (condición2) sentencia2;
else if(condición3) sentencia3;
else sentencia4;

Como se ve, una estructura así anidada se escribe con mayor facilidad y
expresa también más claramente las distintas alternativas. No es
necesario que, en un anidamiento de sentencias condicionales,
encontremos un else final: el último if puede ser una bifurcación
abierta:
if(condición1) sentencia1;
else if (condición2) sentencia2;
else if(condición3) sentencia3;

Un ejemplo de concatenación podría ser el siguiente programa, que


solicita al usuario la nota de un examen y muestra por pantalla la
calificación académica obtenida:
#include <stdio.h>
void main(void)
{
float nota;
printf("Introduzca la nota del examen ... ");
scanf("%f",&nota);
if(nota < 0 || nota > 10) printf("Nota incorrecta.");
else if(nota < 5) printf("Suspenso.");
else if(nota < 7) printf("Aprobado.");
else if(nota < 9) printf("Notable.");
else if(nota < 10) printf("Sobresaliente.");
else printf("Matrícula de honor.");
}

95
Fundamentos de informática. Programación en Lenguaje C

Únicamente se evaluará un else if cuando no haya sido cierta la


condición de ninguno de los anteriores ni del if inicial. Si todas las
condiciones resultan ser falsas, entonces se ejecutará (si existe) el
último else.

• La estructura condicional y el operador condicional

Existe un operador que selecciona entre dos opciones, y que realiza, de


forma muy sencilla y bajo ciertas limitaciones la misma operación que la
estructura de bifurcación cerrada. Es el operador interrogante, dos
puntos (?:).

La sintaxis del operador es la siguiente:

expresión_1 ? expresión_2 : expresión3;

Se evalúa expresión_1; si resulta ser verdadera, entonces se ejecutará


la sentencia recogida en expresión_2; y si es falsa, entonces se
ejecutará la sentencia recogida en expresión_3. Tanto expresión_2
como expresión_3 pueden ser funciones, o expresiones muy complejas,
pero siempre deben ser sentencias simples.

Es conveniente no renunciar a conocer algún aspecto de la sintaxis de


un lenguaje de programación. Es cierto que el operador “interrogante
dos puntos” se puede siempre sustituir por la estructura de control
condicional if – else. Pero el operador puede, en muchos casos,
simplificar el código o hacerlo más elegante. Y hay que tener en cuenta
que el resto de los programadores sí hacen uso del operador y el código
en C está lleno de ejemplos de su uso.

Por ejemplo, el código:


if(x >= 0)
printf(“Positivo\n”);
else
printf(“Negativo\n”);

es equivalente a: printf(“%s\n”, x >= 0 ? “Positivo”: “Negativo”);

96
Capítulo 4. Estructuras de control.

Estructura de selección múltiple: Sentencia switch.


La sentencia switch permite transferir el control de ejecución del
programa a un punto de entrada etiquetado en un bloque de sentencias.
La decisión sobre a qué instrucción del bloque se trasfiere la ejecución
se realiza mediante una expresión entera.

La forma general de la estructura switch es:


switch(variable_del_switch)
{
case expresionConstante1:
[sentencias;]
[break;]
case expresionConstante2:
[sentencias;]
[break;]
[…]
case expresionConstanteN:
[sentencias;]
[break;]
[default
sentencias;]
}

El cuerpo de la sentencia switch se conoce como bloque switch y


permite tener sentencias prefijadas con las etiquetas case. Una etiqueta
case es una constante entera (variables de tipo char ó short ó long,
con o sin signo). Si el valor de la expresión de switch coincide con el
valor de una etiqueta case, el control se transfiere a la primera
sentencia que sigue a la etiqueta. No puede haber dos case con el
mismo valor de constante. Si no se encuentra ninguna etiqueta case
que coincida, el control se transfiere a la primera sentencia que sigue a
la etiqueta default. Si no existe esa etiqueta default, y no existe una
etiqueta coincidente, entonces no se ejecuta ninguna sentencia del
switch y se continúa, si la hay, con la siguiente sentencia posterior a la
estructura.

Por ejemplo, para el código que se muestra a continuación, y cuyo


flujograma queda recogido en la figura 4.5.:

97
Fundamentos de informática. Programación en Lenguaje C

switch(a)
{
case 1: printf(“UNO\t”);
case 2: printf(“DOS\t”);
case 3: printf(“TRES\t”);
default: printf(“NINGUNO\n”);
}

Si el valor de a es, por ejemplo, 2, entonces comienza a ejecutar el


código del bloque a partir de la línea que da entrada el case 2:.
Producirá la siguiente salida por pantalla:

DOS TRES NINGUNO.

No Sí
a=1 “UNO”

No Sí
a=2 “DOS”

No Sí
a=3 “TRES”

“NINGUNO”
Figura 4.5.: Flujograma del
programa ejemplo con switch, sin
F
sentencias break.

Una vez que el control se ha trasferido a la sentencia que sigue a una


etiqueta concreta, ya se ejecutan todas las demás sentencias del bloque
switch, de acuerdo con la semántica de dichas sentencias. El que
aparezca una nueva etiqueta case no obliga a que se dejen de ejecutar
las sentencias del bloque. Si se desea detener la ejecución de sentencias
en el bloque switch, debemos transferir explícitamente el control al
exterior del bloque. Y eso se realiza utilizando la sentencia break.
Dentro de un bloque switch, la sentencia break transfiere el control a
la primera sentencia posterior al switch. Ese es el motivo por el que en
la sintaxis de la estructura switch se escriba (en forma opcional) las
sentencias break en las instrucciones inmediatamente anteriores a cada
una de las etiquetas.

98
Capítulo 4. Estructuras de control.

En el ejemplo anterior, si colocamos la sentencia break en cada case,


switch(a)
{
case 1: printf(“UNO”);
break;
case 2: printf(“DOS”);
break;
case 3: printf(“TRES”);
break;
default: printf(“NINGUNO”);
}

Entonces la salida por pantalla, si la variable a tiene el valor 2 será


únicamente:

DOS

(Puede verse el flujograma de este nuevo código en la figura 4.6.)

No Sí
a=1 “UNO” break;

No Sí
a=2 “DOS” break;

No Sí
a=3 “TRES” break;

“NINGUNO”
Figura 4.6.: Flujograma del
programa ejemplo con switch,
F
con sentencias break.

La ejecución de las instrucciones que siguen más allá de la siguiente


etiqueta case puede ser útil en algunas circunstancias. Pero lo habitual
será que aparezca una sentencia break al final del código de cada
etiqueta case.

Una sola sentencia puede tener más de una etiqueta case. Queda claro
en el siguiente ejemplo:
short int nota;
printf("Introduzca la nota del examen ... ");
scanf("%hd",&nota);

99
Fundamentos de informática. Programación en Lenguaje C

switch(nota)
{
case 1:
case 2:
case 3:
case 4: printf(“SUSPENSO”);
break;
case 5:
case 6: printf(“APROBADO”);
break;
case 7:
case 8: printf(“NOTABLE”);
break;
case 9: printf(“SOBRESALIENTE”);
break;
case 10: printf(“MATRÍCULA DE HONOR”);
break;
default: printf(“Nota introducida errónea.”);
}

No se puede poner una etiqueta case fuera de un bloque switch. Y


tampoco tiene sentido colocar instrucciones dentro del bloque switch
antes de aparecer el primer case: eso supondría un código que jamás
podría llegar a ejecutarse. Por eso, la primera sentencia de un bloque
switch debe estar ya etiquetada.

Se pueden anidar distintas estructuras switch.

El ejemplo de las notas, que ya se mostró al ejemplificar una anidación


de sentencias if–else–if puede servir para comentar una característica
importante de la estructura switch. Esta estructura no admite, en sus
distintas entradas case, ni expresiones lógicas o relacionales, ni
expresiones aritméticas, sino literales. La única relación aceptada es,
pues, la de igualdad. Y además, el término de la igualdad es siempre
entre una variable o una expresión entera (la del switch) y valores
literales: no se puede indicar el nombre de una variable. El programa de
las notas, si la variable nota hubiese sido de tipo float, como de hecho
quedo definida cuando se resolvió el problema con los condicionales if–
else–if no tiene solución posible mediante la estructura switch.

100
Capítulo 4. Estructuras de control.

Y una última observación: las sentencias de un case no forman un


bloque y no tiene porqué ir entre llaves. La estructura switch entera,
con todos sus case’s, sí es un bloque.

Un ejercicio planteado.
Planteamos ahora un ejercicio a resolver: solicite del usuario que
introduzca por teclado un día, mes y año, y muestre entonces por
pantalla el día de la semana que le corresponde.

La resolución de este problema es sencilla si se sabe el cómo. Sin un


correcto algoritmo que nos permita saber cómo procesar la entrada no
podemos hacer nada.

Por lo tanto, antes de intentar implementar un programa que resuelva


este problema, será necesario preguntarse si somos capaces de
resolverlo sin programa. Porque si no sabemos hacerlo nosotros, menos
sabremos explicárselo a la máquina.

Buscando en Internet he encontrado lo siguiente: Para saber a qué día


de la semana corresponde una determinada fecha, basta aplicar la
siguiente expresión:

d = ⎡⎣(26 × M − 2 ) 10 + D + A + A 4 + C 4 − 2 × C ⎤⎦ mod7

Donde d es el día de la semana ( d = 0 es el domingo; d = 1 es el


lunes,…, d = 6 es el sábado); D es el día del mes de la fecha; M es el
mes de la fecha; A es el año de la fecha; y C es la centuria (es decir,
los dos primero dígitos del año) de la fecha.

A esos valores hay que introducirle unas pequeñas modificaciones: se


considera que el año comienza en marzo, y que los meses de enero y
febrero son los meses 11 y 12 del año anterior.

Hagamos un ejemplo a mano: ¿Qué día de la semana fue el 15 de


febrero de 1975?:

101
Fundamentos de informática. Programación en Lenguaje C

• D = 15

• M = 12 : hemos quedado que en nuestra ecuación el mes de febrero


es el décimo segundo mes del año anterior.

• A = 74 : hemos quedado que el mes de febrero corresponde al


último mes del año anterior.

• C = 19

Con todos estos valores, el día de la semana queda:

d = ⎡⎣(26 × 12 − 2 ) 10 + 15 + 74 + 74 4 + 19 4 − 2 × 19⎤⎦ mod7

que es igual a 6, es decir, sábado.

Sólo queda hacer una última advertencia a tener en cuenta a la hora de


calcular nuestros valores de A y de C : Si queremos saber el día de la
semana del 1 de febrero de 2000, tendremos que M = 12 , que A = 99 y
que C = 19 : es decir, primero convendrá hacer las rectificaciones al año
y sólo después calcular los valores de A y de C . Ése día fue…

d = ⎡⎣(26 × 12 − 2 ) 10 + 1 + 99 + 99 4 + 19 4 − 2 × 19⎤⎦ mod7 = 2

es decir… ¡martes!

Queda ahora hacer el programa que nos dé la respuesta al día de la


semana en el que estamos. Hará falta emplear dos veces la estructura
de control condicional if y una vez el switch. El programa queda como
sigue:
#include <stdio.h>

void main(void)
{
unsigned short D, mm, aaaa;
unsigned short M, A, C;

printf("Introduzca la fecha ... \n");


printf("Día ... ");
scanf("%hu", &D);

printf("Mes ... ");


scanf("%hu", &mm);

102
Capítulo 4. Estructuras de control.

printf("Año ... ");


scanf("%hu", &aaaa);

// Valores de las variables:

// El valor de D ya ha quedado introducido por el usuario.


// Valor de M:

if(mm< 3)
{
M = mm + 10;
A = (aaaa - 1) % 100;
C = (aaaa - 1) / 100;
}
else
{
M = mm - 2;
A = aaaa % 100;
C = aaaa / 100;
}

printf("El día %2hu de %2hu de %4hu fue ",D, mm, aaaa);


switch((70+(26*M-2)/10 + D + A + A/4 + C/4 - C*2 ) % 7)
{
case 0: printf("DOMINGO"); break;
case 1: printf("LUNES"); break;
case 2: printf("MARTES"); break;
case 3: printf("MIÉRCOLES"); break;
case 4: printf("JUEVES"); break;
case 5: printf("VIERNES"); break;
case 6: printf("SÁBADO");
}
}

Si, por ejemplo, introducimos la fecha 25 de enero de 1956, la salida del


programa tendrá el siguiente aspecto:
Introduzca la fecha ...
Día ... 25
Mes ... 1
Año ... 1956
El día 25 de 1 de 1956 fue MIÉRCOLES

Falta aclarar por qué he sumado 70 al valor de la expresión que


calculamos en el switch. Veamos un ejemplo para justificar ese valor:
supongamos la fecha 2 de abril de 2001. Tendremos que D = 2 , M = 2 ,
A = 1 y C = 20 , y entonces el valor de d queda: −27%7 que es igual a
−6 . Nuestro algoritmo trabaja con valores entre 0 y 6, y al salir

103
Fundamentos de informática. Programación en Lenguaje C

negativo el valor sobre el que se debe calcular el módulo de 7, el


resultado nos sale fuera de ese rango de valores. Pero la operación
módulo establece una relación de equivalencia entre el conjunto de los
enteros y el conjunto de valores comprendidos entre 0 y el valor del
módulo menos 1. Le sumamos al valor calculado un múltiplo de 7
suficientemente grande para que sea cual sea el valor de las variables,
al final obtenga un resultado positivo. Así, ahora, el valor obtenido será
70 − 27%7 = 43%7 = 1 , es decir, lunes:
Introduzca la fecha ...
Día ... 2
Mes ... 4
Año ... 2001
El día 2 de 4 de 2001 fue LUNES

Estructuras de repetición. Iteración.


Una estructura de repetición o de iteración es aquella que nos permite
repetir un conjunto de sentencias mientras que se cumpla una
determinada condición.

Las estructuras de iteración o de control de repetición, en C, se


implementan con las estructuras do–while, while y for. Todas ellas
permiten la anidación de unas dentro de otras a cualquier nivel. Puede
verse un esquema de su comportamiento en la figura 4.3., en páginas
anteriores.

• Estructura while.

La estructura while, también llamada condicional, o centinela, se


emplea en aquellos casos en que no se conoce por adelantado el
número de veces que se ha de repetir la ejecución de una determinada
sentencia o bloque: ninguna, una o varias.

La sintaxis de la estructura while es la que sigue:

while(condición) sentencia;

104
Capítulo 4. Estructuras de control.

donde condición es cualquier expresión válida en C. Esta expresión se


evalúa cada vez, antes de la ejecución de la sentencia iterada (o bloque
de sentencias si se desea iterar una sentencia compuesta). Puede por
tanto no ejecutarse nunca el bloque de sentencias de la estructura de
control. Las sentencias se volverán a ejecutar una y otra vez mientras
condición siga siendo verdadero. Cuando la condición resulta ser falsa,
entonces el contador de programa se sitúa en la inmediata siguiente
instrucción posterior a la sentencia gobernada por la estructura.

Veamos un ejemplo sencillo. Hagamos un programa que solicite un


entero y muestre entonces por pantalla la tabla de multiplicar de ese
número. El programa es muy sencillo gracias a las sentencias de
repetición:
#include <stdio.h>
void main(void)
{
short int n,i;
printf("Tabla de multiplicar del ... ");
scanf("%hd",&n);
i = 0;
while(i <= 10)
{
printf("%3hu * %3hu = %3hu\n",i,n,i * n);
i++;
}
}

Después de solicitar el entero, inicializa a 0 la variable i y entonces,


mientras que esa variable contador sea menor o igual que 10, va
mostrando el producto del entero introducido por el usuario con la
variable contador i. La variable i cambia de valor dentro del bucle de la
estructura, de forma que llega un momento en que la condición deja de
cumplirse; ¿cuándo?: cuando la variable i tiene un valor mayor que 10.

Conviene asegurar que en algún momento va a dejar de cumplirse la


condición; de lo contrario la ejecución del programa podría quedarse
atrapada en un bucle infinito. De alguna manera, dentro de la
sentencia gobernada por la estructura de iteración, hay que modificar

105
Fundamentos de informática. Programación en Lenguaje C

alguno de los parámetros que intervienen en la condición. Más adelante,


en este capítulo, veremos otras formas de salir de la iteración.

Este último ejemplo ha sido sencillo. Veamos otro ejemplo que requiere
un poco más de imaginación. Supongamos que queremos hacer un
programa que solicite al usuario la entrada de un entero y que entonces
muestre por pantalla el factorial de ese número. Ya se sabe la definición
de factorial: n ! = n ⋅ (n − 1) ⋅ (n − 2) ⋅ ... ⋅ 2 ⋅ 1 .

Antes de mostrar el código de esta sencilla aplicación, conviene volver a


una idea comentada capítulos atrás. Efectivamente, habrá que saber
decir en el lenguaje C cómo se realiza esta operación. Pero previamente
debemos ser capaces de expresar el procedimiento en castellano. En el
capítulo 4 de “Fundamentos de Informática. Codificación y Algoritmia.”
se encuentra extensamente documentado este y otros muchos
algoritmos de iteración que veremos ahora implementados en C, en
estas páginas.

Veamos una posible solución al programa del factorial:


#include <stdio.h>
void main(void)
{
unsigned short n;
unsigned long Fact;
printf("Introduzca el entero ... ");
scanf("%hu",&n);
printf("El factorial de %hu es ... ", n);
Fact = 1;
while(n != 0)
{
Fact = Fact * n;
n = n - 1;
}
printf("%lu.", Fact);

El valor de la variable Fact se inicializa a uno antes de comenzar a


usarla. Efectivamente es muy importante no emplear esa variable sin
darle el valor inicial que a nosotros nos interesa. La variable n se
inicializa con la función scanf.

106
Capítulo 4. Estructuras de control.

Mientras que n no sea cero, se irá multiplicando Fact (inicialmente a


uno) con n. En cada iteración el valor de n se irá decrementando en
uno.

La tabla de los valores que van tomando ambas variables se muestra en


la tabla 4.1. (se supone que la entrada por teclado ha sido el número 5).
Cuando la variable n alcanza el valor cero termina la iteración. En ese
momento se espera que la variable Fact tenga el valor que corresponde
al factorial del valor introducido por el usuario.

La iteración se ha producido tantas veces como el cardinal del número


introducido. Por cierto, que si el usuario hubiera introducido el valor
cero, entonces el bucle no se hubiera ejecutado ni una sola vez, y el
valor de Fact hubiera sido uno, que es efectivamente el valor por
definición ( 0! = 1 ).

n 5 4 3 2 1 0
Fact 5 20 60 120 120 120

Tabla 4.1.: Valores que van tomando las variables del


bucle del programa del cálculo del factorial si la entrada
del usuario ha sido n = 5.

La estructura de control mostrada admite formas de presentación más


compacta y, de hecho, lo habitual será que así se presente. Por
ejemplo:
while(n != 0)
{
Fact *= n;
n = n - 1;
}

donde todo es igual excepto que ahora se ha hecho uso del operador
compuesto en la primera sentencia del bloque. Pero aún se puede
compactar más:

1. La condición de permanencia será verdad siempre que n no sea cero.


Y por definición de verdad en C (algo es verdadero cuando es

107
Fundamentos de informática. Programación en Lenguaje C

distinto de cero) se puede decir que la n != 0 es verdadero sí y sólo


si n es verdadero.

2. Las dos sentencias simples iteradas en el bloque pueden


condensarse en una sola: Fact *= n--;

Así las cosas, la estructura queda:

while(n) Fact *= n--;

Hacemos un comentario más sobre la estructura while. Esta estructura


permite iterar una sentencia sin cuerpo. Por ejemplo, supongamos que
queremos hacer un programa que solicite continuamente del usuario
que pulse una tecla, y que esa solicitud no cese hasta que éste
introduzca el carácter, por ejemplo, ‘a’. La estructura quedará tan
simple como lo que sigue:

while((ch = getchar()) != ‘a’);

Esta línea de programa espera una entrada por teclado. Cuando ésta se
produzca comprobará que hemos tecleado el carácter ‘a’ minúscula; de
no ser así, volverá a esperar otro carácter.

Una forma más sencilla ó fácil de ver el significado de esta última línea
de código vista sería expresarlo de la siguiente manera:
while(ch != ‘a’)
ch = getchar();

Un último ejemplo clásico de uso de la estructura while. El cálculo del


máximo común divisor de dos enteros que introduce el usuario por
teclado.

No es necesario explicar el concepto de máximo común divisor. Sí es


necesario en cambio explicar un método razonable de plantear al
ordenador cómo se calcula ese valor: porque este ejemplo deja claro la
importancia de tener no sólo conocimientos de lenguaje de
programación, sino también de algoritmos válidos.

108
Capítulo 4. Estructuras de control.

Euclides, matemático del siglo V a. de C. presentó un algoritmo muy


fácil de implementar, y de muy bajo coste computacional. El algoritmo
de Euclides dice que el máximo común divisor de dos enteros a1 y b1
(diremos mcd(a1 , b1 ) ), donde b1 ≠ 0 es igual a mcd(a2 , b2 ) donde
a2 = b1 y donde b2 = a1 %b1 , entendiendo por a1 %b1 el resto de la
división de a1 con b1 . Y el proceso puede seguirse hasta llegar a unos
valores de ai y de bi que verifiquen que ai ≠ 0 , bi ≠ 0 y ai %bi = 0 .
Entonces, el algoritmo de Euclides afirma que, llegado a estos valores el
valor buscado es mcd(a1 , b1 ) = bi .

Ahora falta poner esto en lenguaje C. Ahí va:


#include <stdio.h>
void main(void)
{
short int a, b, aux;
short int mcd;
printf("Valor de a ... ");
scanf("%hd",&a);
printf("Valor de b ... ");
scanf("%hd",&b);
printf("El mcd de %hd y %hd es ... ", a, b);
while(b)
{
aux = a % b;
a = b;
b = aux;
}
printf("%hu", a);
}

(De nuevo recordamos que se encuentran en el manual complementario


a éste, titulado “Fundamentos de Informática. Codificación y Algoritmia.”
explicaciones a este nuevo algoritmo y a otros muchos que veremos en
estas páginas.)

Hemos tenido que emplear una variable auxiliar, que hemos llamado
aux, para poder hacer el intercambio de variables: que a pase a valer el
valor de b y b el del resto de dividir a por b.

Así como queda escrito el código, se irán haciendo los intercambios de


valores en las variables a y b hasta llegar a un valor de b igual a cero;

109
Fundamentos de informática. Programación en Lenguaje C

entonces, el anterior valor de b (que está guardado en a) será el


máximo común divisor.

• Estructura do–while.

La estructura do–while es muy similar a la anterior. La diferencia más


sustancial está en que con esta estructura el código de la iteración se
ejecuta, al menos, una vez. Si después de haberse ejecutado, la
condición se cumple, entonces vuelve a ejecutarse, y así hasta que la
condición no se cumpla. Puede verse un esquema de su comportamiento
en la figura 4.3., en páginas anteriores.

La sintaxis de la estructura es la siguiente:

do sentencia while(condición);

Y si se desea iterar un bloque de sentencias, entonces se agrupan en


una sentencia compuesta mediante llaves.

Habitualmente, toda solución a un problema resuelto con una


estructura, es también solventable, de forma más o menos similar, por
cualquiera de las otras dos estructuras. Por ejemplo, la tabla de
multiplicar quedaría:
#include <stdio.h>
void main(void)
{
short int n,i = 0;
printf("Tabla de multiplicar del ... ");
scanf("%hd",&n);
do
{
printf("%3hu * %3hu = %3hu\n",i,n,i * n);
i++:
}while(i <= 10);
}

Una estructura muy habitual en un programa es ejecutar unas


instrucciones y luego preguntar al usuario, antes de terminar la
ejecución de la aplicación, si desea repetir el proceso. Supongamos, por
ejemplo, que queremos un programa que calcule el factorial de tantos
números como desee el usuario, hasta que no quiera continuar. El

110
Capítulo 4. Estructuras de control.

código ahora requiere de otra estructura de repetición, que vuelva a


ejecutar el código mientras que el usuario no diga basta. Una posible
codificación de este proceso sería (ver figura 4.7.):
#include <stdio.h>
void main(void)
{
unsigned short n;
unsigned long Fact;
char opcion;
do
{
Fact = 1;
printf("\n\nIntroduzca el entero ... ");
scanf("%hu",&n);
printf("El factorial de %hu es ... ", n);
while(n != 0) Fact *= n--;
printf("%lu.", Fact);
printf("\n\nCalcular otro factorial (s/n) ");
}while(opcion = getchar() == 's');
}

PROCESO

Sí No
repetir

Figura 4.7.: Flujograma


F
para repetición de proceso.

La estructura do–while repetirá el código que calcula el factorial del


entero solicitado mientras que el usuario responda con una ‘s’ a la
pregunta de si desea que se calcule otro factorial.

Podemos afinar un poco más en la presentación. Vamos a rechazar


cualquier contestación que no sea o ‘s’ o ‘n’: si el usuario responde ‘s’,
entonces se repetirá la ejecución del cálculo del factorial; si responde ‘n’
el programa terminará su ejecución; y si el usuario responde cualquier
otra letra, entonces simplemente ignorará la respuesta y seguirá
esperando una contestación válida.

111
Fundamentos de informática. Programación en Lenguaje C

El código queda ahora de la siguiente manera:


#include <stdio.h>
void main(void)
{
unsigned short n;
unsigned long Fact;
char opcion;
do
{
Fact = 1;
printf("\n\nIntroduzca el entero ... ");
scanf("%hu",&n);
printf("El factorial de %hu es ... ", n);

while(n != 0) Fact *= n--;

printf("%lu.", Fact);
printf("\n\nCalcular otro factorial (s/n) ");

do
opcion = getchar();
while (opcion != ‘s’ && opcion != ‘n’);
}while(opcion = getchar() == 's');
}

Ahora el valor de la variable opcion se irá pidiendo mientras que el


usuario no introduzca correctamente una de las dos respuestas válidas:
o sí (‘s’), o no (‘n’). El flujograma de esta nueva solución queda recogido
en la figura 4.8.

C
C1 ≡ opcion ≠ ' s ' | opcion ≠ ' n '
C 2 ≡ opcion = ' s '

PROCESO

opcion

Sí No
C1

Sí No
C2

Figura 4.8.: Flujograma


para repetición de proceso. F

112
Capítulo 4. Estructuras de control.

• Estructura for.

Una estructura for tiene una sintaxis notablemente distinta a la indicada


para las estructuras while y do–while. Pero la función que realiza es la
misma. Con la palabra reservada for podemos crear estructuras de
control que se dicen “controladas por variable”.

s1

Sí No
e1

s3
F

s2

Figura 4.9.: Flujograma la


estructura de control
for(s1 ; e1 ; s2) s3;

La sintaxis de la estructura for es la siguiente:

for(sentencias_1 , expresión ; sentencias_2) sentencia_3;

Donde sentencia3 es la sentencia que se itera, la que queda


gobernada por la estructura de control for.

Donde sentencias_1 es un grupo de sentencias que se ejecutan antes


que ninguna otra en una estructura for, y siempre se ejecutan una vez
y sólo una vez. Son sentencias, separadas por el operador coma, de
inicialización de variables.

Donde expresión es la condición de permanencia en la estructura


for. Siempre que se cumpla expresión volverá a ejecutarse la sentencia
iterada por la estructura for.

113
Fundamentos de informática. Programación en Lenguaje C

Donde sentencias_2 son un grupo de sentencias que se ejecutan


después de la sentencia iterada (después de sentencia_3).

El orden de ejecución es, por tanto (ver flujograma en figura 4.9.):

1. Se inicializan variables según el código recogido en sentencias_1.

2. Se verifica la condición de permanencia recogida en expresión. Si


expresión es verdadero se sigue en el paso 3; si es falso entonces se
sigue en el paso 6.

3. Se ejecuta la sentencia iterada llamada, en nuestro esquema de


sintaxis, sentencia_3.

4. Se ejecutan las sentencias recogidas en sentencias_2.

5. Vuelta al paso 2.

6. Fin de la iteración.

Así, una estructura for es equivalente a una estructura while de la


forma:
sentencias_1;
while(expresión)
{
sentencia_3;
sentencias_2;
}

Por ejemplo, veamos un programa que muestra por pantalla los enteros
pares del 1 al 100:
#include <stdio.h>
void main(void)
{
short i;
for(i = 2 ; i <= 100 ; i += 2)
printf("%5hd",i);
}

Si queremos mejorar la presentación, podemos hacer que cada cinco


pares comience una nueva fila:
#include <stdio.h>
void main(void)

114
Capítulo 4. Estructuras de control.

{
short i;
for(i = 2 ; i <= 100 ; i += 2)
{
printf("%5hd",i);
if(i % 10 == 0) printf("\n");
}
}

Ya hemos dicho que en cada uno de los tres espacios de la estructura


for destinados a recoger sentencias o expresiones, pueden consignarse
una expresión, o varias, separadas por comas, o ninguna. Y la sentencia
iterada mediante la estructura for puede tener cuerpo, o no. Veamos
por ejemplo, el cálculo del factorial de un entero mediante una
estructura for:

for(Fact = 1 ; n ; Fact *= n--);

Esta estructura for no itera más que la sentencia punto y coma. Toda la
trasformación que deseamos realizar queda en la expresión del cálculo
del factorial mediante la expresión Fact *= n--.

El punto y coma debe ponerse: toda estructura de control actúa


sobre una sentencia. Si no queremos, con la estructura for, controlar
nada, entonces la solución no es no poner nada, sino poner una
sentencia vacía.

Todos los ejemplos que hasta el momento hemos puesto en la


presentación de las estructuras while y do – while se pueden rehacer
con una estructura for. En algunos casos es más cómodo trabajar con la
estructura for; en otros se hace algo forzado. Veamos algunos ejemplos
implementados ahora con la estructura for:

Código para ver la tabla de multiplicar:


#include <stdio.h>
void main(void)
{ short int n,i;
printf("Tabla de multiplicar del ... ");
scanf("%hd",&n);
for(i = 0 ; i <= 10 ; i++)
printf("%3hu * %3hu = %3hu\n",i,n,i * n);
}

115
Fundamentos de informática. Programación en Lenguaje C

Bloquear la ejecución hasta que se pulse la tecla ‘a’:

for( ; ch != ‘a’ ; ch = getchar());

Búsqueda del máximo común divisor de dos enteros:


for( ; b ; )
{
aux = a % b;
a = b;
b = aux;
}
printf("%hu", a);

Sentencias de salto: break y continue.


Hay dos sentencias que modifican el orden del flujo de instrucciones
dentro de una estructura de iteración. Son las sentencias break y
continue. Ambas sentencias, en una estructura de iteración, se
presentan siempre condicionadas.

Una sentencia break dentro de un bloque de instrucciones de una


estructura de iteración interrumpe la ejecución de las restantes
sentencias iteradas y abandona la estructura de control, asignando al
contador de programa la dirección de la siguiente sentencia posterior a
la llave que cierra el bloque de sentencias de la estructura de control.

Por ejemplo, podemos hacer un programa que solicite al usuario que


vaya introduciendo números por teclado hasta que la entrada sea un
número par. En ese momento el programa abandonará la solicitud de
datos e indicará cuántos enteros ha introducido el usuario hasta
introducir uno que sea par. El código podría quedar así:
#include <stdio.h>
void main(void)
{
short i, num;
for(i = 1 ; ; i++)
{
printf("Introduzca un entero ... ");
scanf("%hd",&num);
if(num % 2 == 0) break;

116
Capítulo 4. Estructuras de control.

}
printf("Ha introducido el entero par %hd",num);
printf(" después de %hd impares. ",i - 1);
}

Se habrán introducido tantos enteros como indique la variable i. De


ellos, todos menos el último habrán sido impares. Desde luego, el
código haría lo mismo si la expresión que condiciona al break se
hubiese colocado en el segundo espacio que ofrece la sintaxis del for
(allí donde se colocan las condiciones de permanencia) y se hubiese
cambiado en algo el código; pero no resulta intuitivamente tan sencillo.
Si comparamos la estructura for vista arriba con otra similar, en la que
la condición de salto queda recogida en el for tendremos:
for(i = 1 ; ; i++) for(i = 1 ; num%2 == 0 ; i++)
{ {
printf("entero: "); printf("entero: ");
scanf("%hd",&num); scanf("%hd",&num);
if(num % 2 == 0) break; }
}

Aparentemente ambos códigos hacen lo mismo. Pero si comparamos sus


flujogramas recogidos en la figura 4.10. veremos que se ha introducido
una diferencia no pequeña.

num i ←1 i ← i +1 i ←1
C1 ≡ num mod2 = 0

i ← i +1 C num C

C1 F C1 F

Con break Sin break


Figura 4.10.: Salto con o sin break.

Habitualmente esta sentencia break siempre podrá evitarse con un


diseño distinto de la estructura de control. Según en qué ocasiones, el
código adquiere mayor claridad si se utiliza el break. El uso de la
sentencia de salto break es práctica habitual en la programación
estructurada como es el caso del paradigma del lenguaje C.

117
Fundamentos de informática. Programación en Lenguaje C

Con la sentencia break es posible definir estructuras de control sin


condición de permanencia, o con una condición que es siempre
verdadera. Por ejemplo:
long int suma = 0, num;
do
{
printf(“Introduzca un nuevo sumando ... ”);
scanf(“%ld”,&num);
if(num % 2 != 0) break;
suma += num;
}while(1);
printf(“La suma es ... %ld”, suma);

En esta estructura, la condición de permanencia es verdadera siempre,


por definición, puesto que es un literal diferente de cero. Pero hay una
sentencia break condicionada dentro del bloque iterado. El código irá
guardando en la variable suma la suma acumulada de todos los valores
introducidos por teclado mientras que esos valores sean enteros pares.
En cuanto se introduzca un entero impar se abandona la estructura de
iteración y se muestra el valor sumado.

El diagrama de flujo de este último ejemplo queda recogido en la figura


4.11.

C1 ≡ nummod2 ≠ 0 C

suma ← 0 F

suma ← num suma


suma + num

C1

Figura 4.11.: Otro ejemplo de salto con break.

También se pueden tener estructuras for sin ninguna expresión recogida


entre sus paréntesis. Por ejemplo:
for( ; ; )
{
ch = getchar();

118
Capítulo 4. Estructuras de control.

printf(“Esto es un bucle infinito\n”);


if(ch == ‘a’) break;
}

que repetirá la ejecución del código hasta que se pulse la tecla ‘a’, y que
ocasionará la ejecución del break.

Una sentencia continue dentro de un bloque de instrucciones de una


estructura de iteración interrumpe la ejecución de las restantes
sentencias iteradas y vuelve al inicio de las sentencias de la estructura
de control, si es que la condición de permanencia así lo permite.

Veamos, por ejemplo, el programa antes presentado que muestra por


pantalla los 100 primeros enteros pares positivos. Otro modo de
resolver ese programa podría ser el siguiente:
#include <stdio.h>
void main(void)
{
short i;
for(i = 1 ;i <= 100 ; i++)
{
if(i % 2) continue;
printf("%4hd\t",i);
}
}

Cuando el resto de la división entera entre el contador i y el número 2


es distinto de cero, entonces el número es impar y se solicita que se
ejecute la sentencia continue. Entonces se abandona la ejecución del
resto de las sentencias del bloque iterado y, si la condición de

i ←1

C1
i ← i +1

C2 F

C1 ≡ i ≤ 100 i
C 2 ≡ i mod2 ≠ 0

Figura 4.12.: Salto con continue.

119
Fundamentos de informática. Programación en Lenguaje C

permanencia en la iteración así lo permite, vuelve a comenzar la


iteración por su primera sentencia del bloque iterado. El diagrama de
flujo del código escrito queda recogido en la figura 4.12.

Palabra reservada goto.


La palabra reservada de C goto no debe ser empleada.

La sentencia de salto goto no respeta las reglas de la programación


estructurada. Todo código que se resuelve empleando una sentencia
goto puede encontrar una solución mejor con una estructura de
iteración.

Si alguna vez ha programado con esta palabra, la recomendación es que


se olvide de ella. Si nunca lo ha hecho, la recomendación es que la
ignore.

Y, eso sí: hay que acordarse de que esa palabra es clave en C: no se


puede generar un identificador con esa cadena de letras.

Variables de control de iteraciones.


Hasta el momento, hemos hecho siempre uso de las variables para
poder almacenar valores concretos. Pero pueden tener otros usos. Por
ejemplo, podemos hacer uso de ellas como chivatos o centinelas: su
información no es el valor concreto numérico que puedan tener, sino la
de una situación del proceso.

Como vamos viendo en los distintos ejemplos que se presentan, el


diseño de bucles es tema delicado: acertar en la forma de resolver un
problema nos permitirá llevar solución a muy diversos problemas. Pero,
como estamos viendo, es importante acertar en un correcto control del
bucle: decidir bien cuando se ejecuta y cuando se abandona.

Existen dos formas habituales de controlar un bucle o iteración:

120
Capítulo 4. Estructuras de control.

1. Control mediante variable contador. En ellos una variable se


encarga de contar el número de veces que se ejecuta el cuerpo del
bucle. Esos contadores requieren una inicialización previa, externa al
bucle, y una actualización en cada iteración para llegar así
finalmente a un valor que haga falsa la condición de permanencia.
Esa actualización suele hacerse al principio o al final de las
sentencias iteradas. Hay que garantizar, cuando se diseña una
iteración, que se llega a una situación de salida.

2. Control por suceso. Este tipo de control acepta, a su vez,


diferentes modalidades:

2.1. Consulta explícita: El programa interroga al usuario si desea


continuar la ejecución del bucle. La contestación del usuario
normalmente se almacena en una variable tipo char o int. Es el
usuario, con su contestación, quien decida la salida de la
iteración.

2.2. Centinelas: la iteración se termina cuando la variable de control


toma un valor determinado. Este tipo de control es usado
habitualmente para introducir datos. Cuando el usuario
introduce el valor que el programador ha considerado como
valor de fin de iteración, entonces, efectivamente, se termina
esa entrada de datos. Así lo hemos visto en el ejemplo de
introducción de números, en el programa del cálculo de la
media, en el que hemos considerado que la introducción del
valor cero era entendido como final de la introducción de datos.

2.3. Banderas: Es similar al centinela, pero utilizando una variable


lógica que toma un valor u otro en función de determinadas
condiciones que se evalúan durante la ejecución del bucle. Así se
puede ver, por ejemplo, en el ejercicio 7 planteado al final de
capítulo: la variable que hemos llamado chivato es una variable
bandera.

121
Fundamentos de informática. Programación en Lenguaje C

Normalmente la bandera siempre se puede sustituir por un


centinela, pero se emplea en ocasiones donde la condición de
terminación resulta compleja y depende de varios factores que se
determinan en diferentes puntos del bucle.

Recapitulación.
Hemos presentado las estructuras de control posibles en el lenguaje C.
Las estructuras condicionales de bifurcación abierta o cerrada,
condicionales anidadas, operador interrogante, dos puntos, y sentencia
switch. También hemos visto las posibles iteraciones creadas con las
estructuras for, while y do – while, y las modificaciones a las
estructuras que podemos introducir gracias a las palabras reservadas
break y continue.

El objetivo final de este tema es aprender unas herramientas de enorme


utilidad en la programación. Conviene ahora ejercitarse en ellas,
resolviendo ahora los diferentes ejercicios propuestos.

Ejercicios.
En todos los ejercicios que planteamos a continuación quizá será
conveniente que antes de abordar la implementación se intente diseñar
un algoritmo en pseudocódigo o mediante un diagrama de flujo. Si en
algún caso el problema planteado supone especial dificultad quedará
recogido ese flujograma en estas páginas. En bastantes casos, puede
consultarse éste en las páginas del manual “Fundamentos de
informática. Codificación y algoritmia”.

122
Capítulo 4. Estructuras de control.

20. Calcular la suma de los pares positivos menores o igual a 200.

#include <stdio.h>
void main(void)
{
long suma = 0;
short i;
for(i = 2 ; i <= 200 ; i += 2)
suma += i;
printf("esta suma es ... %ld.\n",suma);
}

21. Hacer un programa que calcule la media de todos los valores


que introduzca el usuario por consola. El programa debe dejar
de solicitar valores cuando el usuario introduzca el valor 0.

#include <stdio.h>
void main(void)
{
long suma;
short i, num;
for(i = 0, suma = 0 ; ; i++)
{
printf("Introduzca número ... ");
scanf("%hd",&num);
suma += num;
if(num == 0) break;
}
if(i == 0) printf("No se han introducido enteros.");
else printf("La media es ... %.2f.",(float)suma / i);
}

En la estructura for se inicializan las variables i y suma. Cada vez que


se introduce un nuevo entero se suma al acumulado de todas las sumas
de los enteros anteriores, en la variable suma. La variable i lleva la
cuenta de cuántos enteros se han introducido; dato necesario para
calcular, cuando se termine de introducir enteros, el valor de la media.

123
Fundamentos de informática. Programación en Lenguaje C

22. Mostrar por pantalla los números perfectos menores de 10000.


Se entiende por número perfecto aquel que es igual a la suma
de sus divisores. Por ejemplo, 6 = 1 + 2 + 3 que son,
efectivamente, sus divisores.

#include <stdio.h>
void main(void)
{
short suma;
short i, num;
for(num = 2 ; num < 10000 ; num++)
{
for(i = 1, suma = 0 ; i <= num / 2 ; i++)
if(num % i == 0) suma += i;
if(num == suma)
printf("En entero %hd es perfecto.\n",num);
}
}

La variable num recorre todos los enteros entre 2 y 10000 en busca de


aquellos que sean perfectos. Esa búsqueda se realiza con el for más
externo.

Para cada valor distinto de num, la variable suma, que se inicializa a


cero cada vez que se comienza de nuevo a ejecutar la estructura for
anidada, guarda la suma de sus divisores. Eso se realiza en el for
anidado.

Después del cálculo de cada suma, si su valor es el mismo que el entero


inicial, entonces ese número será perfecto (esa es su definición) y así se
mostrará por pantalla.

La búsqueda de divisores se hace desde el 1 (que siempre interviene)


hasta la mitad de num: ninguno de los divisores de num puede ser
mayor que su mitad. Desde luego, se podría haber inicializado la
variable suma al valor 1, y comenzar a buscar los divisores a partir del
2, porque, efectivamente, el entero 1 es divisor de todos los enteros.

124
Capítulo 4. Estructuras de control.

23. Solicitar del usuario cuatro números y mostrarlos por pantalla


ordenados de menor a mayor.

#include <stdio.h>
void main(void)
{
unsigned short int a0,a1,a2,a3;
printf("Introduzca cuatro enteros ... \n\n");
printf("Primer entero ... ");
scanf("%hu",&a0);
printf("Segundo entero ... ");
scanf("%hu",&a1);
printf("Tercer entero ... ");
scanf("%hu",&a2);
printf("Cuarto entero ... ");
scanf("%hu",&a3);
if(a0 > a1)
{
a0 ^= a1;
a1 ^= a0;
a0 ^= a1;
}
if(a0 > a2)
{
a0 ^= a2;
a2 ^= a0;
a0 ^= a2;
}
if(a0 > a3)
{
a0 ^= a3;
a3 ^= a0;
a0 ^= a3;
}
if(a1 > a2)
{
a1 ^= a2;
a2 ^= a1;
a1 ^= a2;
}
if(a1 > a3)
{
a1 ^= a3;
a3 ^= a1;
a1 ^= a3;

125
Fundamentos de informática. Programación en Lenguaje C

}
if(a2 > a3)
{
a2 ^= a3;
a3 ^= a2;
a2 ^= a3;
}
printf("\nOrdenados... \n");
printf("%hu <= %hu <= %hu <= %hu.", a0, a1, a2, a3);
}

Donde el código que se ejecuta en cada estructura if intercambia los


valores de las dos variables, como ya vimos en un ejemplo de un tema
anterior.

24. Mostrar por pantalla todos los caracteres ASCII.

(Antes de ejecutar el código escrito, termine de leer todo el texto


recogido en este problema.)
#include <stdio.h>
void main(void)
{
unsigned char a;
for(a = 0 ; a <= 255 ; a++)
printf("%3c - %hX - %hd\n",a,a,a);
}

Va mostrando todos los caracteres, uno por uno, y su código en


hexadecimal y en decimal.

Si se desea ver la aparición de todos los caracteres, se puede programar


para que se pulse una tecla cada vez que queramos que salga el
siguiente carácter por pantalla. Simplemente habría que modificar la
estructura for, de la siguiente forma:
for(a = 0 ; a <= 255 ; a++)
{
printf("%3c - %hX - %hd\n",a,a,a);
getchar();
}

Advertencia importante: De forma intencionada hemos dejado el


código de este problema con un error grave. Aparentemente todo está

126
Capítulo 4. Estructuras de control.

bien. De hecho no existe error sintáctico alguno. El error se verá en


tiempo de ejecución, cuando se compruebe que se ha caído en un bucle
infinito.

Para comprobarlo basta observar la condición de permanencia en la


iteración gobernada por la estructura for, mediante la variable que
hemos llamado a: a <= 255. Si se tiene en cuenta que la variable a es
de tipo unsigned char, no es posible que la condición indicada llegue a
ser falsa, puesto que, en el caso de que a alcance el valor 255, al
incrementar en 1 su valor, incurrimos en overflow, y la variable cae en
el valor cero: (255)10 = (11111111)2 ; si solo tenemos ocho dígitos,
entonces… (11111111 + 1)2 = (00000000)2 , pues no existe el bit noveno,
donde debería haber quedado codificado un dígito 1.

Sirva este ejemplo para advertir de que a veces puede aparecer un


bucle infinito de la manera más insospechada. La tarea de programar no
está exenta de sustos e imprevistos que a veces obligan a dedicar un
tiempo no pequeño a buscar la causa de un error.

Un modo correcto de codificar este bucle sería, por ejemplo:


for(a = 0 ; a < 255 ; a++)
printf("%3c - %hX - %hd\n",a,a,a);
printf("%3c - %hX - %hd\n",a,a,a);

Y así, cuando llegue al valor máximo codificable en la variable a,


abandona el bucle e imprime, ya fuera de la iteración, una última línea
de código, con el valor último.

25. Solicitar al usuario una valor entero por teclado y mostrar


entonces, por pantalla, el código binario en la forma en que se
guarda el número en la memoria.

#include <stdio.h>
void main(void)
{

127
Fundamentos de informática. Programación en Lenguaje C

signed long a;
unsigned long Test;
char opcion;
do
{
Test = 0x80000000;
printf("\n\nIndique el entero ... ");
scanf("%ld", &a);
while(Test)
{
Test & a ? printf("1") : printf("0");
Test >>= 1;
}
printf("\n¿Desea introducir otro entero? ... ");
do
opcion = getchar();
while (opcion != 's' && opcion != 'n');
}while(opcion == 's');
}

La variable Test se inicializa al valor hexadecimal 80000000, es decir,


con un 1 en el bit más significativo y en cero en el resto. Posteriormente
sobre esta variable se va operando un desplazamiento a derecha de un
bit cada vez. Así, siempre tenemos localizado un único bit en la
codificación de la variable Test, hasta que este bit se pierda por la parte
derecha en un último desplazamiento.

Antes de cada desplazamiento, se realiza la operación and a nivel de bit


entre la variable Test, de la que conocemos donde está su único bit, y la
variable de la que queremos conocer su código binario.

En el principio, tendremos así las dos variables:


Test 1000 0000 0000 0000 0000 0000 0000 0000
a xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx
Test & a x000 0000 0000 0000 0000 0000 0000 0000

Tenemos certeza de que la operación and dejará un cero en todos los


bits (menos el más significativo) de Test & a, porque Test tiene todos
esos bits a cero. Todos… menos el primero. Entonces la operación dará
un valor distinto de cero únicamente si en ese bit se encuentra un 1 en
la variable a. Sino, el resultado de la operación será cero.

128
Capítulo 4. Estructuras de control.

Al desplazar ahora Test un bit a la derecha tendremos la misma


situación que antes, pero ahora el bit de a testeado será el segundo por
la derecha.
Test 0100 0000 0000 0000 0000 0000 0000 0000
a xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx
Test & a 0x00 0000 0000 0000 0000 0000 0000 0000

Y así sucesivamente, imprimiremos un 1 cuando la operación and sea


diferente de cero, e imprimiremos un 0 cuando la operación and dé un
valor igual a cero.

El programa que hemos presentado permite al usuario ver el código de


tantos números como quiera introducir por teclado: hay una estructura
do–while que anida todo el proceso.

26. Escribir un programa que solicite al usuario un entero positivo


e indique si ese número introducido es primo o compuesto.

#include <stdio.h>
#include <math.h>
void main(void)
{
unsigned long int numero, raiz;
unsigned long int div;
char chivato;
printf("Dame el numero que vamos a testear ... ");
scanf("%lu", &numero);
chivato = 0;
raiz = sqrt(numero);
for(div = 2 ; div <= raiz ; div++)
{
if(numero % div == 0)
{
chivato = 1;
break;
}
}
if(chivato == 1)
printf("El numero %lu es compuesto",numero);
else printf("El numero %lu es primo",numero);
}

129
Fundamentos de informática. Programación en Lenguaje C

Antes de explicar brevemente el algoritmo, conviene hacer una digresión


sencilla matemática: todo entero verifica que, si tiene divisores distintos
del 1 y del mismo número (es decir, si es compuesto), al menos uno de
esos divisores es menor que su raíz cuadrada. Eso es sencillo de
demostrar por reducción al absurdo: supongamos que tenemos un
entero n y que tiene dos factores distintos de 1 y de n ; por ejemplo, a
y b , es decir, n = a × b . Supongamos que ambos factores son mayores
(estrictamente mayores) que la raíz cuadrada de n . Entonces
tendremos:

n = a× b < n× n =n⇒n<n

En tal caso tendríamos el absurdo de que n es estrictamente menor que


n . Por lo tanto, para saber si un entero n es primo o compuesto, basta
buscar divisores entre 2 y n . Si en ese rango no los encontramos,
entonces podemos concluir que el entero es primo.

En este programa vamos probando con todo los posibles enteros que
dividen al número estudiado, que serán todos los comprendidos entre el
2 y la raíz cuadrada de ese número. En cuanto se encuentra un valor
que divide al entero introducido, entonces ya está claro que ese número
es compuesto y no es menester seguir buscando otros posibles
divisores. Por eso se ejecuta la sentencia break.

Al terminar la ejecución de la estructura for no sabremos si hemos


salido de ella gracias a que hemos encontrado un divisor y nos ha
expulsado la sentencia break, o porque hemos terminado de testear
entre todos los posibles candidatos a divisores menores que la raíz
cuadrada y no hemos encontrado ninguno porque el entero introducido
es primo. Por ello hemos usado la variable chivato, que se pone a 1,
antes de la sentencia break, en caso de que hayamos encontrado un
divisor.

Otro modo de saber si hemos salido del bucle por haber encontrado un
divisor o por haber terminado el recorrido de la variable de control de la

130
Capítulo 4. Estructuras de control.

estructura for es verificar el valor de esa variable contador. Si la


variable div es mayor que raiz entonces está claro que hemos salido del
bucle por haber terminado la búsqueda de posibles divisores y numero
es primo. Si div es menor o igual que raiz, entonces está también claro
que hemos encontrado un divisor: es otra forma de hacer el programa,
sin necesidad de crear la variable chivato.

El código, en ese caso, podría quedar de la siguiente manera:


#include <stdio.h>
#include <math.h>
void main(void)
{
unsigned long int n, raiz;
unsigned long int div;
printf("Dame el numero que vamos a testear ... ");
scanf("%lu", &n);
raiz = sqrt(n);
for(div = 2 ; div<=raiz ; div++) if(n%div == 0) break;
printf(“%lu es %s”, n,n > raiz ? “primo”:“compuesto”);
}

27. Escriba un programa que muestre por pantalla un término


cualquiera de la serie de Fibonacci.

La serie de Fibonacci está definida de la siguiente manera: el primer y el


segundo elementos son iguales a 1. A partir del tercero, cualquier otro
elemento de la serie es igual a la suma de sus dos elementos anteriores.

Escribir el código es muy sencillo una vez se tiene el flujograma. Intente


hacer el flujograma, o consulte página 60 del manual “Fundamentos de
Informática. Codificación y Algoritmia.”
#include <stdio.h>

void main(void)
{
unsigned long fib1 = 1, fib2 = 1, Fib = 1;
unsigned short n, i = 3;

131
Fundamentos de informática. Programación en Lenguaje C

printf("Indique el elemento que se debe mostrar ... ");


scanf("%hu",&n);

while(i <= n)
{
Fib = fib1 + fib2;
fib1 = fib2;
fib2 = Fib;
i++;
}
printf("El término %hu de la serie es %lu.\n",n,Fib);
}

28. Escriba un programa que resuelva una ecuación de segundo


grado. Tendrá como entrada los coeficientes a , b y c de la
ecuación y ofrecerá como resultado las dos soluciones reales.

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

void main(void)
{
float a, b, c;
double r;
// introducción de parámetros...
printf("Introduzca los coeficientes...\n\n");
printf("a --> "); scanf("%f",&a);
printf("b --> "); scanf("%f",&b);
printf("c --> "); scanf("%f",&c);
// Ecuación de primer grado...
if(a == 0)
{ // No hay ecuación ...
if(b == 0) printf("No hay ecuación.\n");
else // Sí hay ecuación de primer grado
{
printf("Ecuación de primer grado.\n");
printf("Tiene una única solución.\n");
printf("x1 --> %lf\n", -c / b);
}
}
// Ecuación de segundo grado. Soluciones imaginarias.
else if ((r = b * b - 4 * a * c) < 0)
{
printf("Ecuación sin soluciones reales.\n");
r = sqrt(-r);

132
Capítulo 4. Estructuras de control.

printf("x1: %lf + %lf * i\n",-b/(2*a), r/(2*a));


printf("x2: %lf + %lf * i\n",-b/(2*a),-r/(2*a));
}
// Ecuación de segundo grado. Soluciones reales.
else
{
printf("Las soluciones son:\n");
r = sqrt(r);
printf("\tx1 --> %lf\n", (-b + r) / (2 * a));
printf("\tx2 --> %lf\n", (-b - r) / (2 * a));
}
}

29. Escriba un programa que muestre todos los divisores de un


entero que se recibe como entrada del algoritmo.

#include <stdio.h>

void main(void)
{
long int numero;
long int div;

printf("Número --> ");


scanf("%ld",&numero);

printf("\n\nLos divisores de %ld son:\n\t", numero);


printf("1, ");

for(div = 2 ; div <= numero / 2 ; div++)


if(numero % div == 0)
printf("%ld, ",div);

printf("%ld.",numero);
}

El código ofrece, por ejemplo, la siguiente salida por pantalla:


Número --> 456
Los divisores de 456 son:
1, 2, 3, 4, 6, 8, 12, 19, 24, 38, 57, 76, 114, 152, 228, 456.

133
Fundamentos de informática. Programación en Lenguaje C

30. Escriba un programa que calcule el número π , sabiendo que


este número verifica la siguiente relación:

π2 ∞
1
6
= ∑ 2.
k =1 k

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

#define LIMITE 10000

void main(void)
{
double PI = 0;

for(int i = 1 ; i < LIMITE ; i++)


PI += 1.0 / (i * i);
PI *= 6;
PI = sqrt(PI);
printf("El valor de PI es ... %lf.",PI);
}

Que ofrece la siguiente salida por pantalla:

El valor de PI es ... 3.141497.

El programa va haciendo, en la iteración gobernada por la estructura


for, el sumatorio de los inversos de los cuadrados. El numerador se
debe poner como 1.0 para que el resultado del cociente no sea un
entero igual a cero sino un valor double.

31. Escriba un programa que calcule el número π , sabiendo que


este número verifica la siguiente relación:

( −1)
k
π ∞

4
= ∑ 2⋅k +1
k =0

134
Capítulo 4. Estructuras de control.

#include <stdio.h>
#define LIMITE 100000

void main(void)
{
double PI = 0;

for(int i = 1 , e = 4 ; i < LIMITE ; i += 2 , e = -e)


PI += e / (double)i;

printf("El valor de PI es ... %lf.",PI);


}

Que ofrece la siguiente salida por pantalla:

El valor de PI es ... 3.141573.

La variable PI se inicializa a cero. En cada iteración irá almacenando la


suma de todos los valores calculados. En lugar de calcular π 4 ,
calculamos directamente el valor de π : por eso el numerador (variable
que hemos llamado e) no varía entre -1 y +1, sino entre -4 y +4. El
valor de la variable i aumenta de dos en dos, y va tomando los
diferentes valores del denominador 2 ⋅ k + 1 . Es necesario forzar el tipo
de la variable i a double, para que el resultado de la operación cociente
no sea un entero, que a partir de i igual a cero daría como resultado el
valor cero.

32. Escriba un programa que calcule el número π , sabiendo que


este número verifica la siguiente relación:

π 2 2 4 4 6 6 8 8
= ⋅ ⋅ ⋅ ⋅ ⋅ ⋅ ⋅ ⋅ ... .
2 1 3 3 5 5 7 7 9

De nuevo el cálculo del valor del número pi. Estos ejercicios son muy
sencillos de buscar (Internet está llena de definiciones de propiedades
del número pi) y siempre es fácil comprobar si hemos realizado un buen
código: basta ejecutarlo y comprobar si sale el famoso 3.14.

En esta ocasión, el código podría tomar la siguiente forma:

135
Fundamentos de informática. Programación en Lenguaje C

#include <stdio.h>
#define LIMITE 100000
void main(void)
{
double PI = 2;
for(int num = 2 , den = 1, i = 1 ;
i < LIMITE ; i++ , i % 2 ? num+=2 : den+=2)
PI *= num / (double)den;
printf("El valor de PI es ... %lf.",PI);
}

Por problemas de espacio se ha tenido que mostrar la estructura for


truncada en dos líneas. Esta forma de escribir es perfectamente válida
en C. El compilador considera lo mismo un espacio en blanco que tres
líneas blancas. Para el compilador estos dos códigos significan lo mismo:
short int a = 0 , b = 1, c = 2;
double x, y, z;
short int
a = 0,
b = 1,
c = 2,
double
x,
y, z;

33. Cinco marineros llegan, tras un naufragio, a una isla desierta


con un gran número de cocoteros y un pequeño mono. Dedican
el primer día a recolectar cocos, pero ejecutan con tanto afán
este trabajo que acaban agotados, por lo que deciden
repartirse los cocos al día siguiente.

Durante la noche un marinero se despierta y, desconfiando de


sus compañeros, decide tomar su parte. Para ello, divide el
montón de cocos en cinco partes iguales, sobrándole un coco,
que regala al mono. Una vez calculada su parte la esconde y se
vuelve a acostar.

Un poco más tarde otro marinero también se despierta y vuelve


a repetir la operación, sobrándole también un coco que regala
al mono. En el resto de la noche sucede lo mismo con los otros

136
Capítulo 4. Estructuras de control.

tres marineros.

Al levantarse por la mañana procedieron a repartirse los cocos


que quedaban entre ellos cinco, no sobrando ahora ninguno.

¿Cuántos cocos habían recogido inicialmente? Mostrar todas las


soluciones posibles menores de 1 millón de cocos.

Este programa es sencillo de implementar. Pero hay que saber resolver


el problema. Es un ejemplo de cómo saber un lenguaje no lo es todo en
programación; más bien podríamos decir que saber un lenguaje es lo de
menos: lo importante es saber qué decir.

Este algoritmo está ya explicado en el otro manual, ya tantas veces


referenciado en éste. Aquí dejamos sólo el código resultante.
#include <stdio.h>

void main(void)
{
unsigned long N = 6, n;
unsigned long soluciones = 0;

printf("Valores posibles de N ...\n\n");

while(N < 4000000)


{
unsigned short int i;
N += 5;
n = N;
for(i = 0 ; i < 5 ; i++)
{
if((n - 1) % 5) break;
n = 4 * (n - 1) / 5;
}
if(i == 5 && !(n % 5))
{
printf("(%4lu) %-8lu",++soluciones, N);
if(!(soluciones % 5)) printf("\n");
}
}
}

137
Fundamentos de informática. Programación en Lenguaje C

34. El calendario juliano (debido a julio Cesar) consideraba que el


año duraba 365.25 días, por lo que se estableció que los años
tendrían una duración de 365 días y cada cuatro años se
añadiese un día más (año bisiesto).

Sin embargo se comprobó que en realidad el año tiene


365.2422 días, lo que implica que el calendario juliano llevase
un desfase de unos once minutos. Este error es relativamente
pequeño, pero, con el transcurrir del tiempo, el error
acumulado puede ser importante.

El papa Gregorio XII, en 1582, propuso reformar el calendario


juliano para evitar los errores arrastrados de años anteriores.
Los acuerdos tomados entonces, que son por los que nos aún
nos regimos, fueron los siguientes:

D Para suprimir el error acumulado por el calendario juliano,


se suprimieron diez días. De tal manera que el día siguiente
al 4 de octubre de 1582 fuel el día 15 del mismo mes.

D La duración de los años sería de 365 días o 366 en caso de


ser bisiesto.

D Serán bisiestos todos los años que sean múltiplos de 4,


salvo los que finalizan en 00, que sólo lo serán cuando
también sean múltiplos de 400, por ejemplo 1800 no fue
bisiesto y el 2000 sí.

D Con esta reforma el desfase existente entre el año civil y el


año real se reduce a menos de treinta segundos anuales.

Definir un algoritmo que solicite al usuario una fecha


introducida mediante tres datos: día, mes y año; ese programa
debe validar la fecha: es decir comprobar que la fecha es
correcta cumpliendo las siguientes reglas:

D El año debe ser mayor que 0.

138
Capítulo 4. Estructuras de control.

D El mes debe ser un número entre uno y doce.

D El día debe estar entre 1 y 30, 31,28 ó 29 dependiendo el


mes de que se trate y si el año es bisiesto o no.

Se deja propuesto. En otro lugar está recogido el flujograma del


algoritmo.

35. Calcule el valor del número e. sabiendo que verifica la siguiente


relación:

1 1 1 1
e= + + + + ...
0! 1! 2! 3!

#include <stdio.h>
void main(void)
{
double p = 1, e = 1;
for(short n = 1 ; n < 100 ; n++)
{
p *= 1.0 / n;
e += p;
}
printf("El numero e es ... %20.17lf.",e);
}

36. Juego de las 15 cerillas: “Participan dos jugadores.


Inicialmente se colocan 15 cerillas sobre una mesa y cada uno
de los dos jugadores toma, alternativamente 1, 2 o 3 cerillas
pierde el jugador que toma la última cerilla”.

Buscar el algoritmo ganador para este juego, generalizando:


inicialmente hay N cerillas y cada vez se puede tomar hasta un
máximo de k cerillas. Los valores N y k son introducidos por

139
Fundamentos de informática. Programación en Lenguaje C

teclado y decididos por un jugador (que será el jugador


perdedor), el otro jugador (que será el ordenador, y que
siempre debe ganar) decide quien empieza el juego.

#include <stdio.h>

void main(void)
{
// Con cuántas cerillas se va a jugar.
unsigned short cerillas;
// Cuántas cerillas se pueden quitar cada vez.
unsigned short quitar;
// Cerillas que quita el ganador y el perdedor.
unsigned short qp, qg;
char ganador;

do
{
printf("\n\n\nCon cuántas cerillas
se va a jugar ... ");
scanf("%hu",&cerillas);
if(cerillas == 0) break;
do
{
printf("\Cuántas cerillas pueden
quitarse de una vez... ");
scanf("%hu",&quitar);
if(quitar >= cerillas)
printf("No pueden quitarse tantas
cerillas.\n");
}while(quitar >= cerillas);

qg = (cerillas - 1) % (quitar + 1);


// MOSTRAR CERILLAS ...
printf("\n");
for(short i = 1 ; i <= cerillas ; i++)
{
printf(" |");
if(!(i % 30))printf("\n\n");
}
printf("\n");
// Fin de MOSTRAR CERILLAS
if(qg)
{
printf("\nComienza la máquina...");
printf("\nMáquina Retira %hu cerillas...
\n", qg);

140
Capítulo 4. Estructuras de control.

cerillas -= qg;
// MOSTRAR CERILLAS ...
printf("\n");
for(short i = 1 ; i <= cerillas ; i++)
{
printf(" |");
if(!(i % 30))printf("\n\n");
}
printf("\n");
}
// FIN DE MOSTRAR CERILLAS
else printf("\nComienza el jugador...");

while(cerillas != 1)
{
do
{
printf("\nCerillas que retira el
jugador ... ");
scanf("%hu",&qp);
if(qp > quitar)
printf("\nNo puede quitar mas
de %hu.\n",quitar);
}while(qp > quitar);
cerillas -= qp;
// MOSTRAR CERILLAS ...
printf("\n");
for(short i = 1 ; i <= cerillas ; i++)
{
printf(" |");
if(!(i % 30)) printf("\n\n");
}
printf("\n");
// Fin de MOSTRAR CERILLAS
if(cerillas == 1)
{
ganador = 'j';
break;
}
qg = quitar - qp + 1;
printf("\nLa máquina retira %hu
cerillas.\n",qg);
cerillas -= qg;
// MOSTRAR CERILLAS ...
printf("\n");
for(short i = 1 ; i <= cerillas ; i++)
{
printf(" |");
if(!(i % 30))printf("\n\n");
}
printf("\n");

141
Fundamentos de informática. Programación en Lenguaje C

// Fin de MOSTRAR CERILLAS


if(cerillas == 1) ganador = 'm';
}

if(ganador == 'j')
printf("\nHa ganado el jugador...");
else if(ganador == 'm')
printf("\nHe ganado yo, la máquina...");
}while(cerillas);
}

El programa ha quedado un poco más largo que los anteriores. Se ha


dedicado un poco de código a la presentación en el momento de la
ejecución. Más adelante, cuando se haya visto el modo de definir
funciones, el código quedará visiblemente reducido. Por ejemplo, en
cuatro ocasiones hemos repetido el mismo código destinado a visualizar
por pantalla las cerillas que quedan por retirar.

37. El número áureo ( Φ ) verifica muchas curiosas propiedades.


Por ejemplo:

Φ2 = Φ + 1 Φ −1 = 1 Φ Φ3 = (Φ + 1) (Φ − 1)

Φ =1+1 Φ Φ = 1+ Φ y otras…

La penúltima expresión presentada ( Φ = 1 + 1 Φ ) muestra un


camino curioso para el cálculo del número áureo:

1 1 1
Si Φ = 1 + ⇒ Φ =1+ ⇒ Φ =1+ ...
Φ 1 1
1+ 1+
Φ 1
1+
Φ

Y, entonces un modo de calcular el número áureo es: inicializar


Φ al valor 1, e ir afinando en el cálculo del valor del número
áureo a base de repetir muchas veces que Φ = 1 + 1 Φ :

142
Capítulo 4. Estructuras de control.

1
Φ 1+
1
1+
1
1+
1
1 + ...
1
1+
1

Escriba un programa para calcular el número áureo mediante


este procedimiento haciendo, por ejemplo, la sustitución
Φ = 1 + 1 Φ mil veces. Mostrar luego el resultado por pantalla.

#include <stdio.h>
#define LIMITE 1000

void main(void)
{
double au = 1;

for(int i = 0 ; i < LIMITE ; i++)


au = 1 + 1 / au;

printf("El número áureo es ..... %lf.\n",au);


printf("Áureo al cuadrado es ... %lf.\n",au * au);
}

No se ha hecho otra cosa que considerar lo que sugiere el enunciado:


inicializar el número áureo a 1, y repetir mil veces la iteración
Φ = 1 + 1 Φ . Al final mostramos también el cuadrado del número áureo:
es un modo rápido de verificar que, efectivamente, el número hallado es
el número buscado: el número áureo verifica que su cuadrado es igual al
número incrementado en uno.

La salida que ofrece este código por pantalla es la siguiente:


El número áureo es ..... 1.618034.
Áureo al cuadrado es ... 2.618034.

38. Siguiendo con el mismo enunciado, también podemos plantearnos


calcular el número áureo a partir de la relación Φ = 1 + Φ . De nuevo

143
Fundamentos de informática. Programación en Lenguaje C

tenemos:

Φ = 1 + Φ = 1 + 1 + Φ = 1 + 1 + 1 + Φ = ... = 1 + 1 + 1 + 1 + 1 + ... 1 + Φ

#include <stdio.h>
#include <math.h>
#define LIMITE 100000

void main(void)
{
double au = 1;

for(int i = 0 ; i < LIMITE ; i++)


au = sqrt(1 + au);

printf("El número áureo es ..... %lf.\n",au);


printf("Áureo al cuadrado es ... %lf.\n",au * au);
}

Este programa ofrece una salida idéntica a la anterior. Y el código es


casi igual que el anterior: únicamente cambia la definición de la
iteración.

39. Muestre por pantalla todos los enteros primos comprendidos


entre dos enteros introducidos por teclado.

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

void main(void)
{
long a, b, div;
printf("Límite inferior ... ");
scanf("%ld",&a);
printf("Límite superior ... ");
scanf("%ld",&b);

printf("Los primos entre %ld y %ld son ...\n\t", a, b);


for(long num = a, primos = 0 ; num <= b ; num++)
{

144
Capítulo 4. Estructuras de control.

for(div = 2 ; div < sqrt(num) ; div++)


if(num % div == 0) break;
if(num % div == 0) continue;
if(primos != 0 && primos % 10 == 0)
printf("\n\t");
primos++;
printf("%6ld,",num);
}
}

El primer for recorre todos los enteros comprendidos entre los dos
límites introducidos por teclado. El segundo for averigua si la variable
num codifica en cada iteración del primer for un entero primo o
compuesto: si al salir del segundo for se tiene que num % div es igual a
cero, entonces num es compuesto y se ejecuta las sentencia continue
que vuelve a la siguiente iteración del primer for. En caso contrario, el
valor de num es primo, y entonces sigue adelante con las sentencias del
primer for, que están destinadas únicamente a mostrar por pantalla, de
forma ordenada, ese entero primo, al igual que habrá mostrado
previamente todos los otros valores primos y mostrará los que siga
encontrando posteriormente.

La salida por pantalla del programa podría ser la siguiente:


Límite inferior ... 123
Límite superior ... 264
Los primos entre 123 y 264 son ...

127, 131, 137, 139, 149, 151, 157, 163, 167, 173,
179, 181, 191, 193, 197, 199, 211, 223, 227, 229,
233, 239, 241, 251, 257, 263,

Para este último programa se recomienda que se dibuje el diagrama de


flujo.

145
Fundamentos de informática. Programación en Lenguaje C

146
CAPÍTULO 5
ÁMBITO Y VIDA DE LAS VARIABLES

Este breve capítulo pretende completar algunos conceptos presentados


en el capítulo 3 y que, una vez hemos visto algo de código en el capítulo
4, serán ahora más sencillos de presentar y de comprender. También
presentamos una breve descripción de cómo se gestiona el
almacenamiento de los datos dentro del ordenador.

Ámbito y Vida.
Entendemos por ámbito de una variable el lugar, dentro de un
programa, en el que esta variable tiene significado. Hasta el momento
todas nuestras variables han tenido como ámbito todo el programa, y
quizá ahora no es sencillo hacerse una idea intuitiva de este concepto;
pero realmente, no todas las variables están “en activo” a lo largo de
todo el programa.
Fundamentos de informática. Programación en Lenguaje C

Además del ámbito, existe otro concepto, que podríamos llamar


extensión o tiempo de vida, que define el intervalo de tiempo en el
que el espacio de memoria reservado por una variable sigue en reserva;
cuando la variable “muere”, ese espacio de memoria vuelve a estar
disponible para otros usos que el ordenador requiera. También este
concepto quedará más aclarado a medida que avancemos en este breve
capítulo.

El almacenamiento de las variables y la memoria.


Para comprender las diferentes formas en que se puede crear una
variable, es conveniente describir previamente el modo en que se
dispone la memoria de datos en el ordenador.

Hay diferentes espacios donde se puede ubicar una variable declarada


en un programa:

1. Registros. El registro es el elemento más rápido de almacenamiento


y acceso a la memoria. La memoria de registro está ubicada
directamente dentro del procesador. Sería muy bueno que toda la
memoria fuera de estas características, pero de hecho el número de
registros en el procesador está muy limitado. El compilador decide
qué variables coloca en estas posiciones privilegiadas. El
programador no tiene baza en esa decisión. El lenguaje C permite
sugerir, mediante algunas palabras clave, la conveniencia o
inconveniencia de que una determinada variable se cree en este
espacio privilegiado de memoria.

2. La Pila. La memoria de pila reside en la memoria RAM (Random


Access Memory: memoria de acceso aleatorio) De la memoria RAM
es de lo que se habla cuando se anuncian “los megas” o “gigas” que
tiene la memoria de un ordenador.

El procesador tiene acceso y control directo a la pila gracias al


“puntero de pila”, que se desplaza hacia abajo cada vez que hay que

148
Capítulo 5. Ámbito y vida de las variables.

reservar más memoria para una nueva variable, y vuelve a


recuperar su posición hacia arriba para liberar esa memoria. El
acceso a la memoria RAM es muy rápido, sólo superado por el
acceso a registros. El compilador debe conocer, mientras está
creando el programa, el tamaño exacto y la vida de todas y cada una
de las variables implicadas en el proceso que se va a ejecutar y que
deben ser almacenados en la pila: el compilador debe generar el
código necesario para mover el puntero de la pila hacia abajo y hacia
arriba. Esto limita el uso de esta buena memoria tan rápida. Hasta el
capítulo 10, cuando hablemos de la asignación dinámica de la
memoria, todas las variables que empleamos pueden existir en la
pila, e incluso algunas de ellas en las posiciones de registro.

3. El montículo. Es un espacio de memoria, ubicada también en la


memoria RAM, donde se crean las variables de asignación dinámica.
Su ventaja es que el compilador no necesita, al generar el programa,
conocer cuánto espacio de almacenamiento necesita asignar al
montículo para la correcta ejecución del código compilado. Esta
propiedad ofrece una gran flexibilidad al código de nuestros
programas. A cambio hay que pagar un precio con la velocidad: lleva
más tiempo asignar espacio en el montículo que tiempo lleva hacerlo
en la pila.

4. Almacenamiento estático. El almacenamiento estático contiene


datos que están disponibles durante todo el tiempo que se ejecuta el
programa. Más adelante, en este capítulo, veremos cómo se crean y
qué características tienen las variables estáticas.

5. Almacenamiento constante. Cuando se define un valor constante,


éste se ubica habitualmente en los espacios de memoria reservados
para el código del programa: lugar seguro, donde no se ha de poder
cambiar el valor de esa constante.

149
Fundamentos de informática. Programación en Lenguaje C

Variables Locales y Variables Globales


Una variable puede definirse fuera de la función principal: en el
programa, pero no en una función. Esas variables se llaman globales, y
son válidas en todo el código que se escriba en ese programa. Su
espacio de memoria queda reservado mientras el programa esté en
ejecución. Diremos que son variables globales, que su ámbito es
todo el programa y que su vida perdura mientras el programa
esté en ejecución.

Veamos como ejemplo el siguiente código:


long int Fact;
#include <stdio.h>
void main(void)
{
short int n;
printf("Introduce el valor de n ... ");
scanf("%hd",&n);
printf("El factorial de %hd es ... ",n);
Fact = 1;
while(n) Fact *=n--;
printf("%ld",Fact);
}

La variable n es local: su ámbito es únicamente el de la función principal


main. La variable Fact es global: su ámbito se extiende a todo el
programa.

Advertencia: salvo para la declaración de variables globales (y


declaración de funciones, que veremos más adelante), el lenguaje C no
admite ninguna otra sentencia fuera de una función.

Ahora mismo este concepto nos queda fuera de intuición porque no


hemos visto aún la posibilidad de crear y definir en un programa otras
funciones, aparte de la función principal. Pero esa posibilidad existe, y
en ese caso, si una variable es definida fuera de cualquier función,
entonces esa variable es accesible desde todas las funciones del
programa.

150
Capítulo 5. Ámbito y vida de las variables.

No se requiere ninguna palabra clave del lenguaje C para indicar al


compilador que esa variable concreta es global.

Se recomienda, en la medida de lo posible, no hacer uso de variables


globales. Cuando una variable es manipulable desde cualquier ámbito
de un programa es fácil sufrir efectos imprevistos.

Una variable será local cuando se crea en un bloque del programa, que
puede ser una función, o un bloque interno de una función.

Por ejemplo:
long x = 12;
// Sólo x está disponible.
{
long y = 25;
// Tanto x como y están disponibles.
}
// La variable y está fuera de ámbito. Ha terminado su vida.

El ámbito de una variable local será el del bloque en el que está


definida. En C, puede declararse una variable local, con un nombre
idéntico al de una variable global; entonces, cuando en ese ámbito local
se haga referencia al nombre de esa variable, se entenderá la variable
local: en ese ámbito no se podrá tener acceso a la variable global, cuyo
nombre “ha sido robado” por una local. Por ejemplo:
long x = 12;
// Sólo x está disponible.
{
long x = 25;
// En este bloque la única variable x accesible vale 25.
}
// La única variable x en este ámbito vale 12.

También pueden definirse variables locales del mismo nombre en


ámbitos diferentes y disjuntos, porque al no coincidir en ámbito en
ninguna sentencia, no puede haber equívoco y cada variable, del mismo
nombre, existe sólo en su propio ámbito. Por ejemplo:
long x = 12;
// Sólo x está disponible.
{
long y = 25;

151
Fundamentos de informática. Programación en Lenguaje C

// Tanto x como y están disponibles.


}
// La variable y está fuera de ámbito. Ha terminado su vida.
{
long y = 40;
// Tanto x como y están disponibles.
// Esta variable y no es la misma que la otra.
/* La declaración de la variable y es correcta, puesto que
la anterior declaración de una variable con el mismo
nombre fue en otro ámbito. */
}
// La variable y está fuera de ámbito. Ha terminado su vida.

Veamos un ejemplo sencillo de uso de diferentes variable locales:


unsigned short i;
for(i = 2 ; i < 10000 ; i++)
{
unsigned short suma = 1;
for(unsigned short j = 2 ; j <= i / 2 ; j++)
if(i % j == 0) suma += j;
if(suma == i)printf(“%hu”,i)
}

En este código, que como vimos permite buscar los números perfectos
entre los primeros 10000 enteros, declara dos variables (j y suma) en el
bloque de la estructura del primer for; la variable j está declarada aún
más local, en el interior del segundo for. Al terminar la ejecución del for
gobernado por la variable i, esas dos variables dejan de existir; la
variable j muere nada más se abandona el espacio de ejecución de la
sentencia iterada por el segundo for. Si a su vez, la estructura for más
externa estuviera integrada dentro de otra estructura de iteración, cada
vez que se volviera a ejecutar ese for se volverían a crear esas dos
variables, que tendrían el mismo nombre, pero no necesariamente las
mismas direcciones de memoria que antes.

Hay una palabra en C para indicar que la variable es local. Es la palabra


reservada auto. Esta palabra rara vez se utiliza, porque el compilador
descubre siempre el ámbito de las variables gracias al lugar donde se
recoge la declaración.

Un ejemplo muy simple puede ayudar a presentar estas ideas de forma


más clara:

152
Capítulo 5. Ámbito y vida de las variables.

#include <stdio.h>

long b = 0, c = 0;
void main(void)
{
for(long b = 0 ; b < 10 ; b++) c++;
printf("El valor de b es %ld y el de c es %ld", b, c);
}

Las variables b y c han sido declaradas globales. Y ambas han sido


inicializadas a cero. Luego, dentro de la función principal, se ha
declarado, local dentro del for, la variable b. Y dentro del for se han
variado los valores de las variables b y c.

¿Cuál es la salida que ofrecerá por pantalla este código? Por lo que
respecta a la variable c no hay ninguna duda: se ha incrementado diez
veces, y su valor, después de ejecutar la estructura for, será 10. Pero,
¿y b? Esta variable ha sufrido también una variación y ha llegado al
valor 10. Pero… ¿cuál de los dos variables b ha cambiado?: la de ámbito
más local. Y como la sentencia que ejecuta la función printf ya está
fuera de la estructura for, y para entonces la variable local b ya ha
muerto, la variable b que muestra la función printf no es otra que la
global: la única viva en este momento. La salida que mostrará el
programa es la siguiente: El valor de b es 0 y el de c es 10.

Una advertencia importante: ya se ha visto que se pueden declarar, en


ámbitos más reducidos, variables con el mismo nombre que otras que
ya existen en ámbitos más globales. Lo que no se puede hacer es
declarar, en un mismo ámbito, dos variables con el mismo nombre. Ante
esa circunstancia, el compilador dará error y no compilará.

Una última observación sobre las variables locales: El lenguaje C


requiere que todas las variables se definan al principio del
bloque donde tienen su ámbito: esas declaraciones de variables
deben ser las primeras sentencias en cada bloque que tenga variables
locales en él. Así, cuando el compilador crea el bloque, puede asignar el
espacio exacto requerido para esas variables en la pila de la memoria.
En C++ es posible diseminar las declaraciones de las distintas variables

153
Fundamentos de informática. Programación en Lenguaje C

a lo largo del bloque, definiéndolas en el momento en que el


programador requiere de su uso. Si se programa en un entorno de C++,
se podrá por tanto diseminar esas declaraciones; pero eso es propiedad
de C++, y el programa generado no podría ser compilado en un
compilador de C.

Es conveniente por tanto, cuando se pretende aprender a programar en


C, imponerse la disciplina de agrupar todas las declaraciones al principio
de cada bloque, como es exigido en la sintaxis de C. Aunque el
programa compilase en un compilador de C++, el código sería
sintácticamente erróneo desde el punto de vista de un compilador de C.

Variables estáticas y dinámicas.


Con respecto a la extensión o tiempo de vida, las variables pueden ser
estáticas o dinámicas. Será estática aquella variable que una vez
definida, persiste hasta el final de la ejecución del programa. Y será
dinámica aquella variable que puede ser creada y destruida durante la
ejecución del programa.

No se requiere ninguna palabra clave para indicar al compilador que una


variable creada es dinámica. Sí es en cambio necesario indicar al
compilador, mediante la palabra clave static, cuando queremos que una
variable sea creada estática. Esa variable puede ser local, y en tal caso
su ámbito será local, y sólo podrá ser usada cuando se estén ejecutando
sentencias de su ámbito; pero su extensión será la misma que la del
programa, y siempre que se vuelvan a las sentencias de su ámbito allí
estará la variable, ya creada, lista para ser usada. Cuando terminen de
ejecutarse las sentencias de su ámbito esas posiciones de memoria no
serán accesibles, porque estaremos fuera de ámbito, pero tampoco
podrá hacerse uso de esa memoria para otras variables, porque la
variable estática seguirá “viva” y esa posición de memoria sigue

154
Capítulo 5. Ámbito y vida de las variables.

almacenando el valor que quedó de la última vez que se ejecutaron las


sentencias de su ámbito.

Cuando se crea una variable local dentro de una bloque, o dentro de una
función, el compilador reserva espacio para esa variable cada vez que se
llama a la función: mueve en cada ocasión hacia abajo el puntero de pila
tanto como sea preciso para volver a crear esa variable. Si existe un
valor inicial para la variable, la inicialización se realiza cada vez que se
pasa por ese punto de la secuencia.

Si se quiere que el valor permanezca durante la ejecución del programa


entero, y no sólo cada vez que se entra de nuevo en el ámbito de esa
variable, entonces tenemos dos posibilidades: La primera consiste en
crear esa variable como global, extendiendo su ámbito al ámbito de todo
el programa (en este caso la variable no queda bajo control del bloque
donde queríamos ubicarla, o bajo control único de la función que la
necesita, sino que es accesible (se puede leer y se puede variar su
valor) desde cualquier sentencia del programa); La segunda consiste en
crear una variable static dentro del bloque o función. El
almacenamiento de esa variable no se lleva a cabo en la pila sino en el
área de datos estáticos del programa. La variable sólo se inicializa una
vez —la primera vez que se llama a la función—, y retiene su valor entre
diferentes invocaciones.

Veamos el siguiente ejemplo, donde tenemos dos variables locales que


sufren las mismas operaciones: una estática (la variable que se ha
llamado a) y la otra no (la que se ha llamado b):
#include <stdio.h>

void main(void)
{
for(long i = 0 ; i < 3 ; i++)
for(long j = 0 ; j < 4 ; j++)
{
static long a = 0;
long b = 0;

for(long j = 0 ; j < 5 ; j++, a++, b++);

155
Fundamentos de informática. Programación en Lenguaje C

printf("a = %3ld. b = %3ld.\n", a, b);


}
}

El programa ofrece la siguiente salida por pantalla:


a = 5. b= 5.
a = 10. b= 5.
a = 15. b= 5.
a = 20. b= 5.
a = 25. b= 5.
a = 30. b= 5.
a = 35. b= 5.
a = 40. b= 5.
a = 45. b= 5.
a = 50. b= 5.
a = 55. b= 5.
a = 60. b= 5.

Variables en registro.
Cuando se declara una variable, se reserva un espacio de memoria para
almacenar sus sucesivos valores. Cuál sea ese espacio de memoria es
cuestión que no podemos gobernar del todo. Especialmente, como ya se
ha dicho, no podemos decidir cuáles son las variables que deben
ubicarse en los espacios de registro.

Pero el compilador, al traducir el código, puede detectar algunas


variables empleadas de forma recurrente, y decidir darle esa ubicación
preferente. En ese caso, no es necesario traerla y llevarla de la ALU a la
memoria y de la memoria a la ALU cada vez que hay que operar con
ella.

El programador puede tomar parte en esa decisión, e indicar al


compilador que alguna o algunas variables conviene que se ubiquen en
los registros de la ALU. Eso se indica mediante la palabra clave
register.

Si al declarar una variable, se precede a toda la declaración la palabra


register, entonces esa variable queda creada en un registro de la ALU.

156
Capítulo 5. Ámbito y vida de las variables.

Una variable candidata a ser declarada register es, por ejemplo, las
que actúan de contadoras en estructuras for.

También puede ocurrir que no se desee que una variable sea


almacenada en un registro de la ALU. Y quizá se desea indicar al
compilador que, sea cual sea su opinión, una determinada variable no
debe ser almacenada allí sino en la memoria, como una variable
cualquiera normal. Para evitar que el compilador decida otra cosa se le
indica con la palabra volatile.

El compilador tomas las indicaciones de register a título orientativo. Si,


por ejemplo, se ha asignado el carácter de register a más variables que
permite la capacidad de la ALU, entonces el compilador resuelve el
conflicto según su criterio, sin abortar el proceso de compilación.

Variables extern
Aunque estamos todavía lejos de necesitar este tipo de declaración,
presentamos ahora esta palabra clave de C, que hace referencia al
ámbito de las variables.

El lenguaje C permite trocear un problema en diferentes módulos que,


unidos, forman una aplicación. Estos módulos muchas veces serán
programas independientes que después se compilan por separado y
finalmente se “linkan” o se juntan. Debe existir la forma de indicar, a
cada uno de esos programas desarrollados por separado, la existencia
de variables globales comunes para todos ellos. Variables cuyo ámbito
trasciende el ámbito del programa donde se declaran, porque abarcan
todos los programas que luego, “linkados”, darán lugar a la aplicación
final.

Se podrían declarar todas las variables en todos los archivos. C en la


compilación de cada programa por separado no daría error, y asignaría
tanta memoria como veces estuvieran declaradas. Pero en el enlazado
daría error de duplicidad.

157
Fundamentos de informática. Programación en Lenguaje C

Para evitar ese problema, las variable globales que deben permanecer
en todos o varios de los módulos de un programa se declaran como
extern en todos esos módulos excepto en uno, donde se declara como
variable global sin la palabra extern. Al compilar entonces esos
módulos, no se creará la variable donde esté puesta la palabra extern,
y permitirá la compilación al considerar que, en alguno de los módulos
de linkado, esa variable sí se crea. Evidentemente, si la palabra extern
se coloca en todos los módulos, entonces en ninguno se crea la variable
y se producirá error en el linkado.

El identificador de una variable declarada como extern es conveniente


que no tenga más de seis caracteres, pues en los procesos de linkado de
módulos sólo los seis primeros caracteres serán significativos.

En resumen…
Ámbito:

El ámbito es el lugar del código donde las sentencias pueden hacer uso
de una variable.

Una variable local queda declarada en el interior de un bloque. Puede


indicarse ese carácter de local al compilador mediante la palabra auto.
De todas formas, la ubicación de la declaración ofrece suficientes pistas
al compilador para saber de la localidad de cada variable. Su ámbito
queda localizado únicamente a las instrucciones que quedan dentro del
bloque donde ha sido creada la variable.

Una variable es global cuando queda declarada fuera de cualquier


bloque del programa. Su ámbito es todo el programa: cualquier
sentencia de cualquier función del programa puede hacer uso de esa
variable global.

Extensión:

158
Capítulo 5. Ámbito y vida de las variables.

La extensión es el tiempo en que una variable está viva, es decir, en que


esa variable sigue existiendo en la memoria.

Una variable global debe existir mientras el programa esté en marcha,


puesto que cualquier sentencia del programa puede hacer uso de ella.

Una variable local sólo existe en el intervalo de tiempo transcurrido


desde la ejecución de la primera sentencia del bloque donde se ha
creado esa variable y hasta que se sale de ese bloque. Es, tras la
ejecución de la última sentencia del bloque, el momento en que esa
variable desaparece. Si el bloque vuelve a ejecutarse entonces vuelve a
crearse una variable con su mismo nombre, que se ubicará donde antes,
o en otra dirección de memoria diferente: es, en todo caso, una variable
diferente a la anterior.

Se puede forzar a que una variable local exista durante toda la ejecución
del programa. Eso puede hacerse mediante la palabra reservada de C
static. En ese caso, al terminar la ejecución de la última instrucción del
bloque donde está creada, la variable no desaparece. De todas formas,
mientras no se vuelva a las sentencias de ese bloque, esa variable no
podrá ser reutilizada, porque fuera de ese bloque, aún estando viva,
está fuera de su ámbito.

Ubicación: Podemos indicar al compilador si queremos que una variable


sea creada en los registros de la ALU, utilizando la palabra reservada
register. Podemos indicarla también al compilador que una variable no
se cree en esos registros, mediante la palabra reservada volatile. Fuera
de esas indicaciones que da el programador, el compilador puede decidir
qué variables se crean en la ALU y cuáles en la memoria principal.

No se ha dicho nada en este capítulo sobre la creación de espacios de


memoria con valores constantes. Ya se presentó la forma de hacerlo en
el capítulo 2 sobre tipos de datos y variables en C. Una variable
declarada como const quedará almacenada en el espacio de memoria
de las instrucciones. No se puede modificar (mediante el operador

159
Fundamentos de informática. Programación en Lenguaje C

asignación) el valor de una variable definida como const. Por eso, al


crear una variable de esta forma hay que asignarle valor en su
declaración.

Ejercicios

40. Haga un programa que calcule el máximo común divisor de dos


enteros que el usuario introduzca por consola. El usuario podrá
hacer tantos cálculos como quiera, e interrumpirá la búsqueda
de nuevos máximos comunes divisores cuando introduzca un
par de ceros.

El programa mostrará por pantalla, cada vez que ejecute el


código del bucle, un valor contador que se incrementa y que
indica cuántas veces se está ejecutando a lo largo de toda la
aplicación. Esa variable contador será declarada como static.

#include <stdio.h>
void main(void)
{
unsigned short a, b, mcd;
do
{
printf("Valor de a ... ");
scanf("%hu",&a);
printf("Valor de b ... ");
scanf("%hu",&b);
if(a == 0 && b == 0) break;
while(b)
{
static unsigned short cont = 0;
mcd = b;
b = a % b;
a = mcd;
cont++;
printf("\ncont = %hu", cont); }
printf("\n\nEl mcd es %hu.", mcd);
}while(1);

160
Capítulo 5. Ámbito y vida de las variables.

Cada vez que se ejecuta el bloque de la estructura do–while se


incrementa en uno la variable cont. Esta variable se inicializa a cero
únicamente la primera vez que se ejecuta la sentencia while de cálculo
del máximo común divisor.

Observación: quizá podría ser interesante, que al terminar de ejecutar


todos los cálculos que desee el usuario, entonces se mostrara por
pantalla el número de veces que se ha entrado en el bucle. Pero eso no
es posible tal y como está el código, puesto que fuera del ámbito de la
estructura while que controla el cálculo del máximo común divisor, la
variable cont, sigue viva, pero estamos fuera de ámbito y el compilador
no reconoce ese identificador como variable existente.

161
Fundamentos de informática. Programación en Lenguaje C

162
CAPÍTULO 6
ARRAYS NUMÉRICOS: VECTORES Y
MATRICES

Hasta el momento hemos trabajado con variables, declaradas una a una


en la medida en que nos han sido necesarias. Pero pudiera ocurrir que
necesitásemos un bloque de variables grande, por ejemplo para definir
los valores de una matriz numérica, o para almacenar los distintos
valores obtenidos en un proceso de cálculo del que se obtienen
numerosos resultados del mismo tipo. Supongamos, por ejemplo, que
deseamos hacer un programa que ordene mil valores enteros: habrá
que declarar entonces mil variables, todas ellas del mismo tipo, y todas
ellas con un nombre diferente.

Es posible hacer una declaración conjunta de un grupo de variables. Eso


se realiza cuando todas esas variables son del mismo tipo. Esa es,
intuitivamente, la noción del array. Y ese es el concepto que
introducimos en el presente capítulo
Fundamentos de informática. Programación en Lenguaje C

Noción y declaración de array.


Un array (también llamado vector) es una colección de variables del
mismo tipo todas ellas referenciadas con un nombre común.

La sintaxis para la declaración de un vector es la siguiente:

tipo nombre_vector[dimensión];

Donde tipo define el tipo de dato de todas las variables creadas, y


dimensión es un literal que indica cuántas variables de ese tipo se deben
crear. En ningún caso está permitido introducir el valor de la dimensión
mediante una variable. El compilador reserva el espacio necesario para
almacenar, de forma contigua, tantas variables como indique el literal
dimensión: reservará, pues, tantos bytes como requiera una de esas
variables, multiplicado por el número de variables a crear.

Por ejemplo, la sentencia short int mi_vector[1000]; reserva dos


mil bytes de memoria consecutivos para poder codificar mil variables de
tipo short.

Cada una de las variables de un array tiene un comportamiento


completamente independiente de las demás. Su única relación con todas
las otras variables del array es que están situadas todas ellas de forma
correlativa en la memoria. Cada variable tiene su propio modo de ser
llamada: desde nombre_vector[0] hasta nombre_vector[dimensión – 1].
En el ejemplo anterior, tendremos 1000 variables que van desde
mi_vector[0] hasta mi_vector[999].

C no comprueba los límites del vector. Es responsabilidad del


programador asegurar que no se accede a otras posiciones de memoria
contiguas del vector. Por ejemplo, si hacemos referencia al elemento
mi_vector[1000], el compilador no dará como erróneo ese nombre,
aunque de hecho no exista tal variable.

164
Capítulo 6. Arrays numéricos: vectores y matrices.

La variable mi_vector[0] está posicionada en una dirección de memoria


cualquiera. La variable mi_vector[1] está situada dos bytes más
adelante, porque mi_vector es un array de tipo short y por tanto
mi_vector[0] ocupa 2 bytes (como todos los demás elementos del
vector), y porque mi_vector[1] es consecutiva en la memoria, a
mi_vector[0]. Esa sucesión de ubicaciones sigue en adelante, y la
variable mi_vector[999] estará 1998 bytes por encima de la posición de
mi_vector[0]. Si hacemos referencia a la variable mi_vector[1000]
entonces el compilador considera la posición de memoria situada 2000
bytes por encima de la posición de mi_vector[0]. Y de allí tomará valor o
escribirá valor si así se lo indicamos. Pero realmente, en esa posición el
ordenador no tiene reservado espacio para esta variable, y no sabemos
qué estaremos realmente leyendo o modificando. Este tipo de errores
son muy graves y a veces no se detectan hasta después de varias
ejecuciones.

El recorrido del vector se puede hacer mediante índices. Por ejemplo:


short mi_vector[1000], i;
for(i = 0 ; i < 1000 ; i++) mi_vector[i] = 0;

Este código recorre todo el vector e inicializa a cero todas y cada una de
sus variables.

Téngase cuidado, por ejemplo, con el recorrido del vector, que va desde
el elemento 0 hasta el elemento dimensión – 1. Un error habitual es
escribir el siguiente código:
short mi_vector[1000], i;
for(i = 0 ; i <= 1000 ; i++) mi_vector[i] = 0;

Donde se hará referencia a la posición 1000 del vector, que no es válida.

Existe otro modo de inicializar los valores de un vector o array, sin


necesidad de recorrerlo con un índice. Se puede emplear para ello el
operador asignación, dando, entre llaves y separados por comas, tantos
valores como dimensión tenga el vector. Por ejemplo;

short mi_vector[10] = {10,20,30,40,50,60,70,80,90,100};

165
Fundamentos de informática. Programación en Lenguaje C

que es equivalente al siguiente código:


short mi_vector[10], i;
for(i = 0 ; i < 10 ; i++) mi_vector[i] = 10 * (i + 1);

Cuando se inicializa un vector mediante el operador asignación en su


declaración, como hay que introducir entre llaves tantos valores como
sea la dimensión del vector creado, es redundante indicar la dimensión
entre corchetes y también en el cardinal del conjunto de valores
asignado. Por eso, se puede declarar ese vector sin especificar el
número de variables que se deben crear. Por ejemplo:

short mi_vector[] = {10,20,30,40,50,60,70,80,90,100};

Por lo demás, estas variables son exactamente iguales que todas las
vistas hasta el momento. También ellas pueden ser declaradas globales
o locales, o static, o extern.

Noción y declaración de array de dimensión


múltiple, o matrices.
Es posible definir arrays de más de una dimensión. El comportamiento
de esas variables vuelve a ser conceptualmente muy sencillo. La sintaxis
de esa declaración es la siguiente:

tipo nombre_matriz[dim_1][dim_2]…[dim_N];

Donde los valores de las dimensiones son todos ellos literales.

Por ejemplo podemos crear una matriz tres por tres:

float matriz[3][3];

que reserva 9 bloques de cuatro bytes cada uno para poder almacenar
valores tipo float. Esas variables se llaman también con índices, en este
caso dos índices (uno para cada dimensión) que van desde el 0 hasta el
valor de cada dimensión menos uno.

Por ejemplo:
long matriz[5][2], i, j;

166
Capítulo 6. Arrays numéricos: vectores y matrices.

for(i = 0 ; i < 5 ; i++)


for(j = 0 ; j < 2 ; j++)
matriz[i][j] = 0;

donde tenemos una matriz de cinco filas y dos columnas, toda ella con
los valores iniciales a cero. También se puede inicializar la matriz
mediante el operador asignación y llaves. En este caso se haría lo
siguiente:

long int matriz[5][2] = {{1,2},{3,4},{5,6},{7,8},{9,10}};

que es lo mismo que escribir


long matriz[5][2], i, j, k;
for(i = 0 , k = 1; i < 5 ; i++)
for(j = 0 ; j < 2 ; j++)
{
matriz[i][j] = k;
k++;
}

Y de nuevo hay que estar muy vigilante para no sobrepasar, al utilizar


los índices, la dimensión de la matriz. Para comprender mejor cómo se
distribuyen las variables en la memoria, y el peligro de equivocarse en
los índices que recorren la matriz, veamos el siguiente programa que
crea una matriz de 2 por 5 y muestra por pantalla la dirección de cada
uno de los 10 elementos:
#include <stdio.h>
void main(void)
{
char m[2][5];
short i, j;
for(i = 0 ; i < 2 ; i++)
for(j = 0 ; j < 5 ; j++)
printf("&m[%hd][%hd] = %p\n",i,j,&m[i][j]);
}

La salida por pantalla ha sido esta:


&m[0][0] = 0012FF80
&m[0][1] = 0012FF81
&m[0][2] = 0012FF82
&m[0][3] = 0012FF83
&m[0][4] = 0012FF84
&m[1][0] = 0012FF85
&m[1][1] = 0012FF86

167
Fundamentos de informática. Programación en Lenguaje C

&m[1][2] = 0012FF87
&m[1][3] = 0012FF88
&m[1][4] = 0012FF89

Donde vemos que los elementos van ordenados desde el m[0][0] hasta
el m[0][4], y a continuación el m[1][0]: cuando termina la primera fila
comienza la segunda fila.

Si ahora, por equivocación, escribiéramos el siguiente código


#include <stdio.h>
void main(void)
{
char m[2][5];
short i, j;
for(i = 0 ; i < 2 ; i++)
for(j = 0 ; j < 6 ; j++)
printf("&m[%hd][%hd] = %p\n",i,j,&m[i][j]);
}

(donde, como se ve, el índice j recorre hasta el valor 5, y no sólo hasta


el valor 4) tendríamos la siguiente salida por pantalla:
&m[0][0] = 0012FF80
&m[0][1] = 0012FF81
&m[0][2] = 0012FF82
&m[0][3] = 0012FF83
&m[0][4] = 0012FF84
&m[0][5] = 0012FF85
&m[1][0] = 0012FF85
&m[1][1] = 0012FF86
&m[1][2] = 0012FF87
&m[1][3] = 0012FF88
&m[1][4] = 0012FF89
&m[1][5] = 0012FF8A

Donde el compilador “se ha tragado” que la matriz tiene el elemento


m[0][5] y el m[1][5]. No sabemos qué habrá en la posición de la
variable no existente m[1][5]. Sí sabemos en cambio qué hay en la
m[0][5]: si vemos la lista de la salida, tenemos que el compilador
considera que la variable m[0][5] estará a continuación de la m[0][4].
Pero por otro lado, ella sabe que la segunda fila comienza en m[1][0] y
sabe dónde está ubicada. Si comparamos &m[0][5] y &m[1][0] veremos
que a ambos se les supone la misma dirección. Y es que m[0][5] no
ocupa lugar porque no existe. Pero cuando se escriba en el código

168
Capítulo 6. Arrays numéricos: vectores y matrices.

m[0][5] = 0;

lo que estaremos poniendo a cero, quizá sin saberlo, es la variable


m[1][0].

Conclusión: mucho cuidado con los índices al recorrer matrices y


vectores.

Ejercicios

41. Escriba el código necesario para crear una matriz identidad


(todos sus valores a cero, excepto la diagonal principal) de
dimensión 3.

short identidad[3][3] = {{1,0,0},{0,1,0},{0,0,1}};

Otra forma de solventarlo:


short identidad[3][3], i, j;
for(i = 0 ; i < 3 ; i++)
for(j = 0 ; j < 3 ; j++)
identidad[i][j] = i == j ? 1 : 0;

El operador ?: se presentó al hablar de las estructuras de control


condicionales. Como el lenguaje C devuelve el valor 1 cuando una
expresión se evalúa como verdadera, hubiera bastando con que la
última línea del código presentado fuese
identidad[i][j] = i == j;

Para mostrar la matriz por pantalla el código es siempre más o menos el


mismo:
for(i = 0 ; i < 3 ; i++)
{
printf("\n\n");
for(j = 0 ; j < 3 ; j++)
printf("%5hd",identidad[i][j]);
}

169
Fundamentos de informática. Programación en Lenguaje C

42. Escriba un programa que solicite al usuario los valores de una


matriz de tres por tres y muestre por pantalla la traspuesta de
esa matriz introducida.

#include <stdio.h>
void main(void)
{
short matriz[3][3];
short i, j;
for(i = 0 ; i < 3 ; i++)
for(j = 0 ; j < 3 ; j++)
{
printf("matriz[%hd][%hd] = ", i, j);
scanf("%hd",&matriz[i][j]);
}
for(i = 0 ; i < 3 ; i++)
{
printf("\n\n");
for(j = 0 ; j < 3 ; j++)
printf("%5hd",matriz[i][j]);
}
printf("\n\n\n");
for(i = 0 ; i < 3 ; i++)
{
printf("\n\n");
for(j = 0 ; j < 3 ; j++)
printf("%5hd",matriz[j][i]);
}
}

Primero muestra la matriz tal y como la ha introducido el usuario, y más


abajo muestra su traspuesta.

43. Escriba un programa que solicite al usuario los valores de dos


matrices de tres por tres y muestre por pantalla cada una de
ellas, una al lado de la otra, y su suma, y cada una de ellas, una
al lado de la otra, y su producto.

170
Capítulo 6. Arrays numéricos: vectores y matrices.

#define TAM 3
#include <stdio.h>
void main(void)
{
short a[TAM][TAM];
short b[TAM][TAM];
short s[TAM][TAM];
short p[TAM][TAM];
short i, j, k;
// Entrada matriz a.
for(i = 0 ; i < TAM ; i++)
for(j = 0 ; j < TAM ; j++)
{
printf("a[%hd][%hd] = ", i, j);
scanf("%hd",&a[i][j]);
}
// Entrada matriz b.
for(i = 0 ; i < TAM ; i++)
for(j = 0 ; j < TAM ; j++)
{
printf("b[%hd][%hd] = ", i, j);
scanf("%hd",&b[i][j]);
}
// Cálculo Suma.
for(i = 0 ; i < TAM ; i++)
for(j = 0 ; j < TAM ; j++)
s[i][j] = a[i][j] + b[i][j];
// Cálculo Producto.
// p[i][j]=a[i][0]*b[0][j]+a[i][1]*b[1][j]+a[i][2]*b[2][j]
for(i = 0 ; i < TAM ; i++)
for(j = 0 ; j < TAM ; j++)
{
p[i][j] = 0;
for(k = 0 ; k < TAM ; k++)
p[i][j] += a[i][k] * b[k][j];
}
// Mostrar resultados.
// SUMA
for(i = 0 ; i < TAM ; i++)
{
printf("\n\n");
for(j = 0 ; j < TAM ; j++)
printf("%4hd",a[i][j]);
printf("\t");
for(j = 0 ; j < TAM ; j++)
printf("%4hd",b[i][j]);
printf("\t");
for(j = 0 ; j < TAM ; j++)
printf("%4hd",s[i][j]);
printf("\t");
}

171
Fundamentos de informática. Programación en Lenguaje C

// PRODUCTO
printf("\n\n\n");
for(i = 0 ; i < TAM ; i++)
{
printf("\n\n");
for(j = 0 ; j < TAM ; j++)
printf("%4hd",a[i][j]);
printf("\t");
for(j = 0 ; j < TAM ; j++)
printf("%4hd",b[i][j]);
printf("\t");
for(j = 0 ; j < TAM ; j++)
printf("%4hd",p[i][j]);
printf("\t");
}
}

En el manejo de matrices y vectores es frecuente utilizar siempre, como


estructura de control de iteración, la opción for. Y es que tiene una
sintaxis que permite manipular muy bien las iteraciones que tienen un
número prefijado de repeticiones. Más, en el caso de las matrices, que
las mismas variables de control de la estructura son las que sirven para
los índices que recorre la matriz.

44. Escriba un programa que solicite al usuario un conjunto de


valores (tantos como quiera el usuario) y que al final, ordene
esos valores de menor a mayor. El usuario termina su entrada
de datos cuando introduzca el cero.

#include <stdio.h>
void main(void)
{
short datos[1000];
short i, j, nn;
// Introducción de datos.
i = 0;
do
{
printf("Entada de nuevo dato ... ");
scanf("%hi",&datos[i]);
i++;

172
Capítulo 6. Arrays numéricos: vectores y matrices.

}while(datos[i - 1] != 0 && i < 1000);


nn = i - 1; // Total de datos válidos introducidos.
// Ordenar datos
for(i = 0 ; i <= nn ; i++)
for(j = i + 1 ; j < nn ; j++)
if(datos[i] > datos[j])
{
datos[i] ^= datos[j];
datos[j] ^= datos[i];
datos[i] ^= datos[j];
}
// Mostrar datos ordenados por pantalla
printf("\n\n");
for(i = 0 ; i < nn ; i++)
printf("%li < ", datos[i]);
printf("\b\b ");
}

Introducción de datos: va solicitando uno a uno todos los datos,


mediante una estructura de control do–while. La entrada de datos
termina cuando la condición es falsa: o cuando se haya introducido un
cero o cuando se hayan introducido tantos valores como enteros se han
creado en el vector. Se habrán introducido tantos datos como indique el
valor de la variable i, donde hay que tener en cuenta que ha sufrido un
incremento también cuando se ha introducido el cero, y ese último valor
no nos interesa. Por eso ponemos la variable nn al valor i – 1.

Ordenar datos: Tiene una forma parecida a la que se presentó para la


ordenación de cuatro enteros (en el tema de las estructuras de control),
pero ahora para una cantidad desconocida para el programador
(recogida en la variable nn). Por eso se deben recorrer todos los valores
mediante una estructura de iteración, y no como en el ejemplo de los
cuatro valores que además no estaban almacenados en vectores, y por
lo tanto no se podía recorrer los distintos valores mediante índices. Los
datos quedan almacenados en el propio vector, de menor a mayor.

Mostrar datos: Se va recorriendo el vector desde el principio hasta el


valor nn: esos son los elementos del vector que almacenan datos
introducidos por el usuario.

173
Fundamentos de informática. Programación en Lenguaje C

45. Escribir un programa que solicite al usuario un entero positivo


e indique si ese número introducido es primo o compuesto.
Además, si el número es compuesto, deberá guardar todos sus
divisores y mostrarlos por pantalla.

#include <stdio.h>
#define TAM 1000
void main(void)
{
unsigned long int numero, mitad;
unsigned long int i;
unsigned long int div;
unsigned long int D[TAM];
for(i = 0 ; i < TAM ; i++) D[i] = 0;
D[0] = 1;
printf("Numero que vamos a testear ... ");
scanf("%lu", &numero);
mitad = numero / 2;
for(i = 1 , div = 2 ; div <= mitad ; div++)
{
if(numero % div == 0)
{
D[i] = div;
i++;
if(i == TAM)
{
printf("Vector mal dimensionado.");
break;
}
}
}
if(i < TAM) D[i] = numero;
if(i == 1) printf("\n%lu es PRIMO.\n",numero);
else
{
printf("\n%lu es COMPUESTO. ", numero);
printf("Sus divisores son:\n\n");
for(i = 0 ; i < TAM && D[i] != 0; i++)
printf("\n%lu", D[i]);
}
}

Este programa es semejante a uno presentado en el capítulo de las


estructuras de control. Allí se comenta el diseño de este código. Ahora
añadimos que, cada vez que se encuentra un divisor, se almacena en

174
Capítulo 6. Arrays numéricos: vectores y matrices.

una posición del vector D y se incrementa el índice del vector (variable


i). Se inicia el contador al valor 1 porque a la posición 0 del vector ya se
le ha asignado el valor 1.

La variable i hace de centinela y de chivato. Si después de buscar todos


los divisores la variable i está al valor 1, entonces es señal de que no se
ha encontrado ningún divisor distinto del 1 y del mismo número, y por
tanto ese número es primo.

Para la dimensión del vector se utiliza una constante definida con la


directiva de procesador define. Si se desea cambiar ese valor, no será
necesario revisar todo el código en busca de las referencias a los límites
de la matriz, sino que todo el código está ya escrito sobre ese valor
prefijado. Basta cambiar el valor definido en la directiva para que se
modifiquen todas las referencias al tamaño del vector.

46. Escribir un programa que defina un array de short de 32


elementos, y que almacene en cada uno de ellos los sucesivos
dígitos binarios de un entero largo introducido por pantalla.
Luego, una vez obtenidos todos los dígitos, el programa
mostrará esos dígitos.

Un posible código que da solución a este programa podría er el


siguiente:
#include <stdio.h>

void main(void)
{
signed long N;
unsigned short bits[32], i;
unsigned long Test;

do
{
printf("\n\nIntroduce un entero ... ");

175
Fundamentos de informática. Programación en Lenguaje C

scanf("%li",&N);

if(N == 0) break;

for(i=0,Test = 0x80000000 ; Test ; Test >>=1,i++)


bits[i] = Test & N ? 1 : 0;

printf("\nCodificación binaria interna ... ");


for(i = 0 ; i < sizeof(long) * 8 ; i++)
printf("%hu", bits[i]);

}while(1);
}

Este código permite introducir tantos enteros como quiera el usuario.


Cuando el usuario introduzca el valor cero entonces se termina la
ejecución del programa. Ya quedó explicado el funcionamiento de este
algoritmo en un tema anterior. Ahora simplemente hemos introducido la
posibilidad de que se almacenen los dígitos binarios en un array.

Una posible salida por pantalla de este programa sería la siguiente:


Introduce un entero ... 12
Codificación binaria interna ... 00000000000000000000000000001100
Introduce un entero ... -12
Codificación binaria interna ... 11111111111111111111111111110100
Introduce un entero ... 0

47. Un cuadro mágico es un reticulado de n filas y n columnas que


tiene la propiedad de que todas sus filas, y todas sus columnas,
y las diagonales principales, suman el mismo valor. Por
ejemplo:

6 1 8

7 5 3

2 9 4

La técnica que se utiliza para generar cuadros mágicos


(que tienen siempre una dimensión impar: impar número

176
Capítulo 6. Arrays numéricos: vectores y matrices.

de filas y de columnas) es la siguiente:

a. Se comienza fijando el entero 1 en el espacio central de


la primera fila.

b. Se van escribiendo los sucesivos números (2, 3, ...)


sucesivamente, en las casillas localizadas una fila
arriba y una columna a la izquierda. Estos
desplazamientos se realizan tratando a la matriz como
si estuviera envuelta sobre sí misma, de forma que
moverse una posición hacia arriba desde la fila superior
lleva a la fila inferior, y moverse una posición a la
izquierda desde la primera columna lleva a la columna
más a la derecha del cuadro.

c. Si se llega a una posición ya ocupada (es decir, si


arriba a la izquierda ya está ocupado con un número
anterior), entonces la posición a rellenar cambia, que
ahora será la inmediatamente debajo de la última
casilla rellenada. Después se continúa el proceso tal y
como se ha descrito en el punto anterior.

Escriba un programa que genere el cuadro mágico de la


dimensión que el usuario desee, y lo muestre luego por
pantalla..

Para llegar a una solución para este programa ofrecemos el flujograma


desglosado en partes. Está recogido en la figura 6.1. Con él se puede
implementar fácilmente el código que imprima el cuadro mágico. El
primer paso (que el usuario introduzca el valor de la dimensión de la
matriz cuadrada) debería hacerse de tal manera que sólo se acepta un
valor que sea impar; en caso contrario, el programa vuelve a solicitar
una dimensión: y así hasta que el usuario acierta a introducir un valor
impar.

177
Fundamentos de informática. Programación en Lenguaje C

Aparte del código que cada uno pueda escribir de la mano del
flujograma, ofrecemos ahora otro que agiliza de forma notable la
búsqueda de la siguiente posición del cuadro donde se ha de colocar el
siguiente valor de numero. En el código se ha definido una macro
mediante la directiva #define. No es trivial verlo a la primera, pero
ayuda el ejemplo a comprender que a veces un código bien pensado
facilita su comprensión.

El código es el siguiente:
#include <stdio.h>
#define lr(x, N) ((x)< 0 ? N+(x)%N : ((x)>=N ? (x)%N : (x) ))

void main(void)
{
unsigned short magico[17][17];
unsigned short fil, col, dim, num;

do
{
printf( "\nDimensión ( impar entre 3 y 17 ): ");
scanf("%hu", &dim);
}while(dim % 2 == 0);

printf( "\nCuadro Mágico de dimensión %hu:\n\n", dim);

//Inicializamos la matriz a cero

for(fil = 0 ; fil < dim ; fil++)


for(col = 0 ; col < dim ; col++)
magico[fil][col] = 0;

// Algoritmo de asignación de valores...

for(fil = dim/2 , col = 0 , num = 1 ; num < dim*dim;)


{
if(magico[fil][col] == 0)
{
magico[fil][col] = num++;
fil = lr(fil + 1, dim);
col = lr(col - 1, dim);
}
else
{
fil = lr(fil - 1, dim);
col = lr(col + 2, dim);
}
}

178
Capítulo 6. Arrays numéricos: vectores y matrices.

// Mostramos ahora el cuadrado mágico por pantalla

for(fil = 0 ; fil < dim ; fil++)


{
printf("\n\n");
for(col = 0 ; col < dim ; col++)
printf("%5hu", magico[fil][col]);
}
}

179
Fundamentos de informática. Programación en Lenguaje C

C C C

dim fil ← 0
fil ← 0

C1 ≡ fil < dim Salto de línea


Inicializar matriz
M Sí No
C1 C1 ≡ fil < dim
Sí No
C1
col ← 0
Rellenar matriz F
M col ← 0
F
M[fil ][col ] ← 0
Mostrar matriz M[fil ][col ]
M No Sí
C2
No C2 Sí
C 2 ≡ col < dim
C 2 ≡ col < dim
F

Proceso general Iniciar Matriz M Mostrar Matriz M

C fil ← dim 2 col ← 0 M[fil ][col ] ← 1 numero ← 2 F

numero ← numero + 1

M[fil , col ] ← numero Sí No


C1
Buscar siguiente
C1 ≡ numero < dim2
posición (fil , col )
Rellenar Matriz M

C posf ← fil posc ← col


C1 ≡ fil = 0 C 2 ≡ col = 0
Sí No Sí No
C1 C2
Buscar siguiente
posición (fil , col ) fil ← dim − 1 fil ← fil − 1 col ← dim − 1 col ← col − 1

F No Si
C3
C 3 ≡ M[fil ][col ] ≠ 0

M[fil ][col ] ← numero col ← posc + 1 fil ← posf + 1

Figura 6.1.: Flujograma para la implementación del cuadro mágico.

180
CAPÍTULO 7
CARACTERES Y CADENAS DE
CARACTERES

Ya hemos visto que un carácter se codifica en C mediante el tipo de dato


char. Es una posición de memoria (la más pequeña que se puede
reservar en C: un byte) que codifica cada carácter según un código
arbitrario. El más extendido en el mundo del PC y en programación con
lenguaje C es el código ASCII.

Hasta el momento, cuando hemos hablado de los operadores de una


variable char, hemos hecho referencia al uso de esa variable (que no se
ha recomendado) como entero de pequeño rango o dominio. Pero donde
realmente tiene uso este tipo de dato es en el manejo de caracteres y
en el de vectores o arrays declarados de este tipo.

A un array de tipo char se le suele llamar cadena de caracteres.

En este capítulo vamos a ver las operaciones básicas que se pueden


realizar con caracteres y con cadenas. No hablamos de operadores,
Fundamentos de informática. Programación en Lenguaje C

porque estos ya se han visto antes, en un capítulo previo, y se reducen


a los aritméticos, lógicos, relacionales y a nivel de bit. Hablamos de
operaciones habituales cuando se manejan caracteres y sus cadenas:
operaciones que están definidas como funciones en algunas bibliotecas
del ANSI C y que presentaremos en este capítulo.

Operaciones con caracteres.


La biblioteca ctype.h contiene abundantes funciones para la
manipulación de caracteres. En la ayuda on line que ofrece cualquier
editor de C se pueden encontrar indicaciones concretas y prácticas para
el uso de cada una de ellas.

La biblioteca ctype.h define funciones de manejo de caracteres de


forma individual, no concatenados formando cadenas.

Lo más indicado será ir viendo cada una de esas funciones y explicar


qué operación realiza sobre el carácter y qué valores devuelve.

• int isalnum(int c);

Recibe el código ASCII de una carácter y devuelve el valor 1 si el


carácter que corresponde a ese código ASCII es una letra o un dígito
numérico; en caso contrario devuelve un 0.

Ejemplo de uso:
if(isalnum(‘@’)) printf(“Alfanumérico”):
else printf(“No alfanumérico”);

Así podemos saber si el carácter ‘@’ es considerado alfabético o


numérico. La respuesta será que no lo es. Evidentemente, para hacer
uso de esta función y de todas las que se van a ver en este epígrafe,
hay que indicar al compilador la biblioteca en donde se encuentran
definidas estas bibliotecas: #define <ctype.h>.

• int isalpha(int c);

182
Capítulo 7. Caracteres y cadenas de caracteres.

Recibe el código ASCII de una carácter y devuelve el valor 1 si el


carácter que corresponde a ese código ASCII es una letra; en caso
contrario devuelve un 0.

Ejemplo de uso:
if(isalnum(‘2’)) printf(“Alfabético”):
else printf(“No alfabético”);

La respuesta será que ‘2’ no es alfabético.

• int iscntrl(int c);

Recibe el código ASCII de una carácter y devuelve el valor 1 si el


carácter que corresponde a ese código ASCII es el carácter borrado o un
carácter de control (ASCII entre 0 y 1F, y el 7F, en hexadecimal); en
caso contrario devuelve un 0.

Ejemplo de uso:
if(iscntrl(‘\n’)) printf(“Carácter de control”):
else printf(“No carácter de control”);

La respuesta será que el salto de línea sí es carácter de control.

Resumidamente ya, presentamos el resto de funciones de esta


biblioteca. Todas ellas reciben como parámetro el código ASCII de un
carácter.

• int isdigit(int c); Devuelve el valor 1 si el carácter es un dígito; en


caso contrario devuelve un 0.

• int isgraph(int c); Devuelve el valor 1 si el carácter es un carácter


imprimible; en caso contrario devuelve un 0.

• int isascii(int c); Devuelve el valor 1 si el código ASCII del carácter es


menor de 128; en caso contrario devuelve un 0.

• int islower(int c); Devuelve el valor 1 si el carácter es una letra


minúscula; en caso contrario devuelve un 0.

• int ispunct(int c); Devuelve el valor 1 si el carácter es signo de


puntuación; en caso contrario devuelve un 0.

183
Fundamentos de informática. Programación en Lenguaje C

• int isspace(int c); Devuelve el valor 1 si el carácter es el espacio en


blanco, tabulador vertical u horizontal, retorno de carro, salto de línea, u
otro carácter de significado espacial en un texto; en caso contrario
devuelve un 0.

• int isupper(int c); Devuelve el valor 1 si el carácter es una letra


mayúscula; en caso contrario devuelve un 0.

• int isxdigit(int c); Devuelve el valor 1 si el carácter es un dígito


hexadecimal (del ‘0’ al ‘9’, de la ‘a’ a la ‘f’ ó de la ‘A’ a la ‘F’); en caso
contrario devuelve un 0.

• int tolower(int ch); Si el carácter recibido es una letra mayúscula,


devuelve el ASCII de su correspondiente minúscula; en caso contrario
devuelve el mismo código ASCII que recibió como entrada.

• int toupper(int ch); Si el carácter recibido es una letra minúscula,


devuelve el ASCII de su correspondiente mayúscula; en caso contrario
devuelve el mismo código ASCII que recibió como entrada.

Con estas funciones definidas se pueden trabajar muy bien muchas


operaciones que se pueden realizar con caracteres. Por ejemplo, veamos
un programa que solicita caracteres por consola, hasta que recibe el
carácter salto de línea, y que únicamente muestra por pantalla los
caracteres introducidos que sean letras. Al final indicará cuántas veces
han sido pulsadas cada una de las cinco vocales.
#include <stdio.h>
#include <ctype.h>
void main(void)
{
unsigned short int a = 0, e = 0, i = 0, o = 0, u = 0;
char ch;
do
{
ch = getchar();
if(isalpha(ch))
{
if(ch == 'a') a++;
else if(ch == 'e') e++;
else if(ch == 'i') i++;
else if(ch == 'o') o++;

184
Capítulo 7. Caracteres y cadenas de caracteres.

else if(ch == 'u') u++;


}
else
printf("\b ");
}while(ch != 10);
printf("\n\nVocal a ... %hu",a);
printf("\nVocal e ... %hu",e);
printf("\nVocal i ... %hu",i);
printf("\nVocal o ... %hu",o);
printf("\nVocal u ... %hu",u);
}

Si el carácter introducido es alfabético, entonces simplemente verifica si


es una vocal y aumenta el contador particular para cada vocal. Si no lo
es, entonces imprime en pantalla un retorno de carro y un carácter
blanco, de forma que borra el carácter que había sido introducido
mediante la función getchar y que no deseamos que salga en pantalla.

El carácter intro es el ASCII 13. Así lo hemos señalado en la condición


que regula la estructura do–while.

Entrada de caracteres.
Hemos visto dos funciones que sirven bien para la introducción de
caracteres.

La función scanf espera la entrada de un carácter más el carácter intro.


No es muy cómoda cuando se espera únicamente un carácter.

La función getchar también está definida en la biblioteca stdio.h. De


todas formas, su uso no es siempre el esperado, por problemas del
buffer de teclado. Un buffer es como una cola o almacén que crea y
gestiona el sistema operativo. Nuestro programas nunca actúan
directamente sobre la máquina, sino siempre a través de los servicios
que ofrece ese sistema operativo. Con frecuencia ocurre que la función
getchar debería esperar la entrada por teclado de un carácter, pero no
lo hace porque ya hay caracteres a la espera en el buffer gestionado por
el sistema operativo.

185
Fundamentos de informática. Programación en Lenguaje C

Si se está trabajando con un editor en el sistema operativo Windows, se


puede hacer uso de la biblioteca conio.h y de algunas de sus funciones
de entrada por consola. Esta biblioteca no es estándar en ANSI C, pero
si vamos a trabajar en ese sistema operativo sí resulta válida.

En esa biblioteca vienen definidas dos funciones útiles para la entrada


por teclado, y que no dan los problemas que da, en Windows, la función
getchar.

Esas dos funciones son:

int getche(void); espera del usuario un pulso de teclado. Devuelve el


código ASCII del carácter pulsado y muestra por pantalla ese carácter.
Al ser invocada esta función, no recibe valor o parámetro alguno: por
eso se define como de tipo void.

int getch(void); espera del usuario un pulso de teclado. Devuelve el


código ASCII del carácter pulsado. Al ser invocada esta función, no
recibe valor o parámetro alguno: por eso se define como de tipo void.
Esta función no tiene eco en pantalla, y no se ve el carácter pulsado.

Cadena de caracteres.
Una cadena de caracteres es una formación de caracteres. Es un vector
tipo char, cuyo último elemento es el carácter nulo (ó NULL, ó ‘\0’ se
escribe en C). Toda cadena de caracteres termina con el carácter
llamado carácter NULO de C. Este carácter indica donde termia la
cadena.

La declaración de una cadena de caracteres se realiza de forma similar a


la de un vector de cualquier otro tipo:

char mi_cadena[dimensión];

donde dimensión es un literal que indica el número de bytes que se


deben reservar para la cadena (recuerde que una variable tipo char
ocupa un byte).

186
Capítulo 7. Caracteres y cadenas de caracteres.

La asignación de valores, cuando se crea una cadena, puede ser del


mismo modo que la asignación de vectores:

char mi_cadena[4] = {‘h’,’o’,’l’,’a’};

Y así hemos asignado a la variable mi_cadena el valor de la cadena


“hola”.

Y así, con esta operación de asignación, acabamos de cometer un error


importante. Repasemos… ¿qué es una cadena?: es un vector tipo char,
cuyo último elemento es el carácter nulo. Pero si el último elemento es
el carácter nulo… ¿no nos hemos equivocado en algo?: Sí.

La correcta declaración de esta cadena sería:

char mi_cadena[5] = {‘h’,’o’,’l’,’a’,’\0’};

Faltaba el carácter nulo con el que debe terminar toda cadena. De todas
formas, la asignación de valor a una cadena suele hacerse mediante
comillas dobles, de la siguiente manera:

char mi_cadena[5] = “hola”;

Y ya en la cadena “hola” se recoge el quinto carácter: el carácter nulo.


Ya se encarga el compilador de C de introducir el carácter nulo al final
de la cadena.

Y es que hay que distinguir, por ejemplo, entre el carácter ‘a’ y la


cadena “a”. En el primer caso tratamos del valor ASCII 97, que codifica
la letra ‘a’ minúscula; en el segundo caso tratamos de una cadena de
dos caracteres: el carácter ‘a’ seguido del carácter nulo.

También podríamos haber hecho lo siguiente:

char mi_cadena[100] = “hola”;

donde todos los bytes posteriores al carácter nulo tendrán valores


aleatorios, no asignados. Pero eso no importa, porque la cadena sólo se
extiende hasta el carácter nulo: no nos interesa lo que pueda haber más
allá de ese carácter.

187
Fundamentos de informática. Programación en Lenguaje C

Y al igual que con los arrays, podemos inicializar la cadena, sin


necesidad de dimensionarla:

char mi_cadena[] = “hola”;

Y entonces ya se encarga el compilador de reservar cinco bytes para la


variable mi_cadena.

Una forma de vaciar una cadena y asignarle el valor de cadena vacía es


el siguiente;

mi_cadena[0] = ‘\0’;

donde, ya lo hemos dicho, ‘\0’ es el carácter nulo.

Definimos longitud de la cadena como el número de caracteres


previos al carácter nulo. El carácter nulo no cuenta como parte para el
cálculo de la longitud de la cadena. La cadena “hola” necesita cinco
variables char para ser almacenada, pero su longitud se dice que es
cuatro. Asignando al elemento de índice 0 el valor nulo, tenemos una
cadena de longitud cero.

Cada elemento de la cadena se reconoce a través del índice entre


corchetes. Cuando se quiere hacer referencia a toda la cadena en su
conjunto, se emplea el nombre sin ningún corchete ni índice.

Dar valor a una cadena de caracteres.


Para recibir cadenas por teclado disponemos, entre otras, de la función
scanf ya presentada en capítulos anteriores. Esa función toma la cadena
introducida por teclado, pero la corta a partir de la primera entrada de
un carácter en blanco. Se puede evitar ese corte, pero en general, para
introducir por teclado una cadena que puede tener caracteres en blanco,
muchos compiladores de C recomiendan el uso de otra función, mucho
más cómoda, que es la función gets.

188
Capítulo 7. Caracteres y cadenas de caracteres.

La función gets está definida en la biblioteca stdio.h, y su prototipo es


el siguiente:

char *gets(char *s);

Esta función asigna a la cadena s todos los caracteres introducidos como


cadena. La función queda a la espera de que el usuario introduzca la
cadena de texto. Hasta que no se pulse la tecla intro, se supone que
todo lo que se teclee será parte de la cadena de entrada. Al final de
todos ellos, como es natural, coloca el carácter nulo.

Hay que hacer una advertencia grave sobre el uso de esta función:
puede ocurrir que la cadena introducida por teclado sea de mayor
longitud que el número de bytes que se han reservado. Esa incidencia
no es vigilada por la función gets. Y si ocurre, entonces, además de
grabar información de la cadena en los bytes reservados para ello, se
hará uso de los bytes, inmediatamente consecutivos a los de la cadena,
hasta almacenar toda la información tecleada más su carácter nulo final.
Esa violación de memoria puede tener —y de hecho habitualmente
tiene— consecuencias desastrosas para el programa.

Ejemplo: programa que pregunta el nombre y, entonces saluda al


usuario.
#include <stdio.h>
void main(void)
{
char nombre[10];
printf("¿Cómo te llamas? ");
gets(nombre);
printf("Hola, %s.", nombre);
}

El programa está bien construido. Pero hay que tener en cuenta que el
nombre que se introduce puede, fácilmente, superar los 10 caracteres.
Por ejemplo, si un usuario responde diciendo “José Antonio”, ya ha
introducido 13 caracteres: 4 por José, 7 por Antonio, 1 por el carácter
en blanco, y otro más por el carácter nulo final. En ese caso, el
comportamiento del programa sería imprevisible.

189
Fundamentos de informática. Programación en Lenguaje C

Se puede hacer un programa que acepte la entrada por teclado de


carácter a carácter, y vaya mostrando la entrada por pantalla a la vez
que la va almacenando en la cadena. Veamos una posible
implementación.
#define TAM 30
#include <stdio.h>
#include <ctype.h>
void main(void)
{
char nombre[TAM];
short int i;
printf("¿Cómo te llamas? ");
for(i = 0 ; i < TAM ; i++)
nombre[i] = NULL;
i = 0;
while(i < TAM)
{
nombre[i] = getchar();
if (nombre[i] == 10)
{
nombre[i] = NULL;
break;
}
else if(nombre[i] == 8 && i > 0)
{
nombre[i] = NULL;
printf("\b \b");
i--;
}
else if(isgraph(nombre[i]) || nombre[i] == 32)
printf("%c",nombre[i++]);
}
printf("\n\nHola, %s.", nombre);
}

Donde el valor ASCII 8 es el carácter borrar uno hacia detrás: eso


significa que hay que retroceder, escribir un blanco donde ya habíamos
escrito otro carácter, y volver a retroceder; además hay que reducir en
uno el valor del índice i, puesto que hemos eliminado un carácter. Si
estábamos en el carácter 0, la operación de borrado no surte efecto,
porque así lo hemos condicionado en el if correspondiente. El valor
ASCII 10 es el carácter intro, que vamos a entender como final de
entrada de la cadena: por eso se interrumpe el proceso de entrada de
caracteres y se cambia el valor de ese último carácter que deja de ser

190
Capítulo 7. Caracteres y cadenas de caracteres.

10 para ser el carácter nulo. Y el carácter 32 es el carácter espacio en


blanco, que no se resuelve como imprimible en la función isgraph.

Esta pequeña aplicación, e incluso alguna un poco más desarrollada,


podrían resolver el problema de la función gets, pero para nuestro
estudio será mejor usar la función de stdio.h sin complicarse la vida.

Operaciones con cadenas de caracteres.


Todo lo visto en el capítulo de vectores es perfectamente aplicable a las
cadenas: de hecho una cadena no es más que un vector de tipo char.
De todas formas, las cadenas de caracteres merecen un tratamiento
diferente al presentado para el resto de vectores, ya que las operaciones
que se pueden realizar con cadenas son muy diferentes a las que se
realizan con vectores numéricos: concatenar cadenas, buscar una
subcadena en una cadena dada, determinar cuál de dos cadenas es
mayor alfabéticamente, etc. Vamos a ver algunos programas básicos de
manejo de cadena, y posteriormente presentaremos un conjunto de
funciones de biblioteca definidas para las cadenas, y que se encuentran
en la biblioteca string.h.

• Copiar el contenido de una cadena en otra cadena:


#include <stdio.h>
void main(void)
{
char original[100];
char copia[100];
short int i = 0;
printf(“Cadena original ... ”);
gets(original);
while(original[i] != NULL)
{
copia[i] = original[i];
i++;
}
copia[i] = NULL;
printf("Original: %s\n",original);
printf("Copìa: %s\n",copia);
}

191
Fundamentos de informática. Programación en Lenguaje C

Mientras no se llega al carácter nulo de original, se van copiando uno a


uno los valores de las variables de la cadena en copia. Al final, cuando
ya se ha llegado al nulo en original, habrá que cerrar también la cadena
en copia, mediante un carácter nulo.

El carácter nulo se escribe NULL, ó ‘\0’. Ambas formas son equivalentes.

Esta operación también se puede hacer con una función de la biblioteca


string: la función

char *strcpy(char *dest, const char *src);

que recibe como parámetros las dos cadenas, origen y destino, y


devuelve la dirección de la cadena de destino. Con esta función, el
programa antes presentado queda de la siguiente forma:
#include <stdio.h>
#include <string.h>
void main(void)
{
char original[100];
char copia[100];
printf(“Cadena original ... ”);
gets(original);
strcpy(copia, original);
printf("Original: %s\n",original);
printf("Copìa: %s\n",copia);
}

• Determinar la longitud de una cadena:


#include <stdio.h>
void main(void)
{
char original[100];
short int i = 0;
printf(“Cadena original ... ”);
gets(original);
while(original[i] != NULL) i++;
printf("%s tiene longitud %hd\n",original,i);
}

El contador i se va incrementando hasta llegar al carácter nulo. Así, en i,


tenemos la longitud de la cadena.

192
Capítulo 7. Caracteres y cadenas de caracteres.

Esta operación también se puede hacer con una función de la biblioteca


string: la función

size_t strlen(const char *s);

que recibe como parámetro una cadena de caracteres y devuelve su


longitud. Una variable tipo size_t es, para nosotros y ahora mismo, una
variable de tipo entero.
#include <stdio.h>
#include <string.h>
void main(void)
{
char original[100];
short int i;
printf(“Cadena original ... ”);
gets(original);
i = strlen(original);
printf("%s tiene longitud %hd\n",original,i);
}

• Concatenar una cadena al final de otra:


#include <stdio.h>
void main(void)
{
char original[100];
char concat[100];
short int i = 0, j = 0;
printf(“Cadena original ... ”);
gets(original);
printf(“Cadena a concatenar ... ”);
gets(concat);
while(original[i] != NULL) i++;
while(concat[j] != NULL)
{
original[i] = concat[j];
i++;
j++;
}
original[i] = NULL;
printf("Texto concatenado: %s\n",original);
}

Y de nuevo disponemos de una función en la biblioteca string que


realiza la concatenación de cadenas:

char *strcat(char *dest, const char *src);

193
Fundamentos de informática. Programación en Lenguaje C

Que recibe como parámetros las cadenas destino de la concatenación y


cadena fuente, y devuelve la dirección de la cadena que contiene la
cadena original más la concatenada.
#include <stdio.h>
#include <string.h>
void main(void)
{
char original[100];
char concat[100];
printf(“Cadena original ... ”);
gets(original);
printf(“Cadena a concatenar ... ”);
gets(concat);
strcat(original, concat);
printf("Texto concatenado: %s\n",original);
}

También existe otra función, parecida a esta última, que concatena no


toda la segunda cadena, sino hasta un máximo de caracteres, fijado por
un tercer parámetro de la función:

char *strncat(char *dest, const char *src, size_t maxlen);

• Comparar dos cadenas e indicar cuál de ellas es mayor, o si son


iguales:
#include <stdio.h>
void main(void)
{
char cad01[100];
char cad02[100];
short int i = 0;
short int chivato = 0;
printf(“Primera Cadena ... ”);
gets(cad01);
printf(“Segunda Cadena ... ”);
gets(cad02);
while(cad01[i] != NULL && cad02[i] != NULL)
{
if(cad01[i] > cad02[i])
{
chivato = 1;
break;
}
else if(cad01[i] < cad02[i])
{
chivato = 2;

194
Capítulo 7. Caracteres y cadenas de caracteres.

break;
}
i++;
}
if(chivato == 1)
printf("cadena01 > cadena02");
else if(chivato == 2)
printf("cadena02 > cadena01");
else if(cad01[i] == NULL && cad02[i] != NULL)
printf("cadena02 > cadena01");
else if(cad01[i] != NULL && cad02[i] == NULL)
printf("cadena01 > cadena02");
else
printf("cadenas iguales");
}

Y una vez más, disponemos de una función en la biblioteca string que


realiza la comparación de cadenas:

int strcmp(const char *s1, const char *s2);

Que recibe como parámetros las cadenas a comparar y devuelve un


valor negativo si s1 < s2; un valor positivo si s1 > s2; y un cero si
ambas cadenas son iguales
#include <string.h>
#include <stdio.h>
void main(void)
{
char c1[100] = "Texto de la cadena primera";
char c2[100] = "Texto de la cadena segunda";
short int comp;
printf(“Primera Cadena ... ”);
gets(c1);
printf(“Segunda Cadena ... ”);
gets(c2);
comp = strcmp(c1,c2);
if(comp < 0) printf("cadena02 > cadena01");
else if(comp > 0) printf("cadena01 > cadena02");
else printf("Cadenas iguales");
}

También existe una función que compara hasta una cantidad de


caracteres señalado, es decir, una porción de la cadena:

int strncmp(const char *s1, const char *s2, size_t maxlen);

195
Fundamentos de informática. Programación en Lenguaje C

donde maxlen es el tercer parámetro, que indica hasta cuántos


caracteres se han de comparar.

Podríamos seguir con otras muchas funciones de la biblioteca string.


Hemos mostrado algunas de las operaciones con cadenas, con su
función y sin ella, para presentar con ejemplos el modo habitual con el
que se manejan las cadenas de caracteres. Hay mucha información
sobre estas y otras funciones de la biblioteca string en cualquier ayuda
on line de cualquier editor de C. Se recomienda consultar esa ayuda
para obtener información sobre ellas.

Otras funciones de cadena.


Vamos a detenernos en la conversión de una cadena de caracteres,
todos ellos numéricos, en la cantidad numérica, para poder luego operar
con ellos. Las funciones que veremos en este epígrafe se encuentran
definidas en otras bibliotecas: en la stdlib.h o en la biblioteca math.h.

• Convertir una cadena de caracteres (todos ellos dígitos o signo decimal)


en un double.

double strtod(const char *s, char **endptr);

Esta función convierte la cadena s en un valor double. La cadena s debe


ser una secuencia de caracteres que puedan ser interpretados como un
valor double. La forma genérica en que se puede presentar esa cadena
de caracteres es la siguiente:

[ws] [sn] [ddd] [.] [ddd] [fmt[sn]ddd]

donde [ws] es un espacio en blanco opcional; [sn] es el signo opcional


(+ ó –); [ddd] son dígitos opcionales; [fmt] es el formato exponencial,
también opcional, que se indica con las letras ‘e’ ó ‘E’; finalmente, el [.]
es el carácter punto decimal, también opcional. Por ejemplo, valores
válidos serían + 1231.1981 e-1; ó 502.85E2; ó + 2010.952.

196
Capítulo 7. Caracteres y cadenas de caracteres.

La función abandona el proceso de lectura y conversión en cuanto llega


a un carácter que no puede ser interpretable como un número real. En
ese caso, se puede hacer uso del segundo parámetro para detectar el
error que ha encontrado: aquí se recomienda que para el segundo
parámetro de esta función se indique el valor nulo: en esta parte del
libro aún no se tiene suficiente formación en C para poder comprender y
emplear bien este segundo parámetro.

La función devuelve, si ha conseguido la transformación, el número ya


convertido en formato double.

Vamos a ver un ejemplo.


#include <stdio.h>
#include <stdlib.h>
void main(void)
{
char entrada[80];
double valor;
printf("Número decimal ... ");
gets(entrada);
valor = strtod(entrada, NULL);
printf("La cadena es %s ", entrada);
printf("y el número es %lf\n", valor);
}

De forma semejante se comporta la función atof, de la biblioteca


math.h. Su prototipo es:

double atof(const char *s);

Donde el formato de la cadena de entrada es el mismo que hemos visto


para la función strtod.

Consultando la ayuda del compilador se puede ver cómo se emplean las


funciones strtol y strtoul, de la biblioteca stdlib.h: la primera
convierte una cadena de caracteres en un entero largo; la segunda es lo
mismo pero el entero es siempre largo sin signo. Y también las
funciones atoi y atol, que convierte la cadena de caracteres a int y a
long int respectivamente.

197
Fundamentos de informática. Programación en Lenguaje C

Como ejemplo de todo lo dicho, veamos un programa que acepta como


entrada una cadena de la forma “x + y =” y muestra por pantalla el
resultado de la operación. Desde luego, el desarrollo presentado es uno
de los muchos posibles; y como se ha dicho ya en otras ocasiones en
este manual, no necesariamente la mejor de las soluciones:
#include <stdio.h>
#include <ctype.h>
#include <stdlib.h>
void main(void)
{
char e[80];
char n[20];
char op;
short i = 0, j = 0;
short est = 0, err = 0;
long int a1, a2, res;
printf("Introduzca la cadena de operación.");
printf("\nX + Y = o X - Y =... ");
gets(e);
while(e[i] != NULL)
{
if(e[i] != 32 && !isdigit(e[i]) && e[i] != '+' &&
e[i] != '-' && e[i] != '=')
{
err = 1;
break;
}
else if(e[i] == 32 && (est == 0 || est == 2 ||
est == 3 || est == 5)) i++;
else if(isdigit(e[i]) && (est == 0 || est == 3))
{
est++;
n[j] = e[i];
i++;
j++;
}
else if(isdigit(e[i]) && (est == 1 || est == 4))
{
n[j] = e[i];
j++;
i++;
}
else if(e[i] == 32 && (est == 1 || est == 4))
{
n[j] = NULL;
if(est == 1) a1 = atol(n);
else a2 = atol(n);
est++;

198
Capítulo 7. Caracteres y cadenas de caracteres.

j = 0;
i++;
}
else if((e[i] == '+' || e[i] == '-') && est == 2)
{
op = e[i];
i++;
est = 3;
}
else if(e[i] == '=' && est == 5)
{
printf("%lu %c %lu = ",a1,op,a2);
printf("%lu", op=='+' ? a1 + a2 : a1 - a2);
break;
}
else
{
err = 1;
break;
}
}
if(err == 1) printf("Error de entrada de datos");
}

Ejercicios.

48. Escribir un programa que solicite del usuario la entrada de una


cadena y muestre por pantalla en número de veces que se ha
introducido cada una de las cinco vocales.

#include <stdio.h>
void main(void)
{
char cadena[100];
short int a, e, i, o, u, cont;
printf("Introduzca una cadena de texto ... \n");
gets(cadena);
a = e = i = o = u = 0;
for(cont = 0 ; cadena[cont] != NULL ; cont++)
{
if(cadena[cont] == 'a') a++;
else if(cadena[cont] == 'e') e++;
else if(cadena[cont] == 'i') i++;

199
Fundamentos de informática. Programación en Lenguaje C

else if(cadena[cont] == 'o') o++;


else if(cadena[cont] == 'u') u++;
}
printf("La cadena introducida ha sido ...\n");
printf("%s\n",cadena);
printf("Y las vocales introducidas han sido ... \n");
printf("a ... %hd\n",a);
printf("e ... %hd\n",e);
printf("i ... %hd\n",i);
printf("o ... %hd\n",o);
printf("u ... %hd\n",u);
}

Una sola observación al código: la asignación concatenada que pone a


cero las cinco variables de cuenta de vocales. Esta sintaxis es correcta y
está permitida en C.

49. Escribir un programa que solicite del usuario la entrada de una


cadena y muestre por pantalla esa misma cadena en
mayúsculas.

#include <stdio.h>
#include <ctype.h>
void main(void)
{
char cadena[100];
short int cont;
printf("Introduzca una cadena de texto ... \n");
gets(cadena);
for(cont = 0 ; cadena[cont] != NULL ; cont++)
cadena[cont] = toupper(cadena[cont]);
printf("La cadena introducida ha sido ...\n");
printf("%s\n",cadena);
}

50. Escribir un programa que solicite del usuario la entrada de una


cadena y luego la imprima habiendo eliminado de ella los
espacios en blanco.

200
Capítulo 7. Caracteres y cadenas de caracteres.

#include <stdio.h>
#include <string.h>
void main(void)
{
char cadena[100];
short int i, j;
printf("Introduzca una cadena de texto ... \n");
gets(cadena);
for(i = 0 ; cadena[i] != NULL ; )
if(cadena[i] == 32)
for(j = i ; cadena[j] != NULL ; j++)
cadena[j] = cadena[j + 1];
else i++;
printf("La cadena introducida ha sido ...\n");
printf("%s\n",cadena);
}

Si el carácter i es el carácter blanco (ASCII 32) entonces no se


incrementa el contador sino que se adelantan una posición todos los
caracteres hasta el final. Si el carácter no es el blanco, entonces
simplemente se incrementa el contador y se sigue rastreando la cadena
de texto.

Una observación: la estructura for situada inmediatamente después de


la función gets, controla una única sentencia simple, que está controlado
por una estructura if–else, que a su vez controla otra sentencia for,
también con una sola sentencia simple. No es necesario utilizar ninguna
llave en todo ese código porque no hay ni una sola sentencia
compuesta.

51. Escribir un programa que solicite del usuario la entrada de una


cadena y la entrada de un desplazamiento. Luego debe volver a
imprimir la cadena con todos los caracteres alfabéticos
desplazados tantas letras en el alfabeto como indique el
desplazamiento. Si en ese desplazamiento se llega más allá de
la letra ‘Z’, entonces se continúa de nuevo con la ‘A’. Se tienen
26 letras en el alfabeto ASCII.

201
Fundamentos de informática. Programación en Lenguaje C

#include <ctype.h>
#include <stdio.h>
void main(void)
{
char c[100];
short int i;
short int d;
printf("Introduzca una cadena de texto ... \n");
gets(c);
for(i = 0 ; c[i] != NULL ; i++)
c[i] = toupper(c[i]);
printf("Introduzca el desplazamiento ... \n");
scanf("%hd",&d);
d %= 26; /* Si d = 26 * k + d', donde d' < 26
entonces desplazar d en el
abecedario es lo mismo que desplazar d'. */
for(i = 0 ; c[i] != NULL ; i++)
{
if(isalpha(c[i]))
{
c[i] += d;
if(c[i] > 'Z') c[i] = 'A' + c[i] - 'Z' - 1;
}
}
printf("La cadena transformada queda ... \n");
printf("%s",c);
}

Una única observación a este código y, en general, a todos los que se


presentan como ejemplo en este manual. Si bien vienen resueltos,
cuando de verdad se aprende a programar es en el momento en que
uno se pone delante de la pantalla y comienza a echar líneas de código
en busca de SU solución. Este programa, como la mayoría, tiene
infinidad de formas diferentes de solventarlo. La que aquí se muestra no
es la mejor, simplemente es la que se ha ocurrido a quien esto escribe
en el tiempo que ha tardado en redactar esa solución. Si hubiera
resuelto el programa mañana, el libro tendría una solución distinta.

Y es que copiar el código en un editor de C y compilarlo no sirve para


aprender. Que este manual es de C, y no de mecanografía.

202
CAPÍTULO 8
PUNTEROS

La memoria puede considerarse como una enorme cantidad de


posiciones de almacenamiento de información perfectamente ordenadas.
Cada posición es un octeto o byte de información. Cada posición viene
identificada de forma inequívoca por un número que suele llamarse
dirección de memoria. Cada posición de memoria tiene una dirección
única. Los datos de nuestros programas se guardan en esa memoria.

La forma en que se guardan los datos en la memoria es mediante el uso


de variables. Una variable es un espacio de memoria reservado para
almacenar un valor: valor que pertenece a un rango de valores posibles.
Esos valores posibles los determina el tipo de dato de esa variable.
Dependiendo del tipo de dato, una variable ocupará más o menos bytes
de la memoria, y codificará la información de una u otra manera.

Si, por ejemplo, creamos una variable de tipo float, estaremos


reservando cuatro bytes de memoria para almacenar sus posibles
valores. Si, por ejemplo, el primero de esos bytes es el de posición
Fundamentos de informática. Programación en Lenguaje C

ABD0:FF31 (es un modo de escribir: en definitiva estamos dando 32 bits


para codificar las direcciones de la memoria), el segundo byte será el
ABD0:FF32, y luego el ABD0:FF33 y finalmente el ABD0:FF34. La
dirección de memoria de esta variable es la del primero de sus bytes; en
este caso, diremos que toda la variable float está almacenada en
ABD0:FF31. Ya se entiende que al hablar de variables float, se emplean
un total de 4 bytes.

Ese es el concepto habitual cuando se habla de la posición de memoria o


de la dirección de una variable.

Además de los tipos de dato primitivos ya vistos en un tema anterior,


existe un C un tipo de dato especial, que ofrece muchas posibilidades y
confiere al lenguaje C de una filosofía propia. Es el tipo de dato puntero.
Mucho tiene que ver ese tipo de dato con la memoria de las variables.
Este capítulo está dedicado a su presentación.

Definición y declaración.
Una variable tipo puntero es una variable que contiene la
dirección de otra variable.

Para cada tipo de dato, primitivo o creado por el programador, permite


la creación de variables puntero hacia variables de ese tipo de dato.
Existen punteros a char, a long, a double, etc. Son nuevos tipos de
dato: puntero a char, puntero a long, puntero a double,…

Y como tipos de dato que son, habrá que definir para ellos un dominio y
unos operadores.

Para declarar una variable de tipo puntero, la sintaxis es similar a la


empleada para la creación de las otras variables, pero precediendo al
nombre de la variable del carácter asterisco (*).

tipo *nombre_puntero;

Por ejemplo:

204
Capítulo 8. Punteros.

short int *p;

Esa variable p así declarada será una variable puntero a short, que no
es lo mismo que puntero a float, etc.

En una misma instrucción, separados por comas, pueden declararse


variables puntero con otras que no lo sean:

long a, b, *c;

Se han declarado dos variables de tipo long y una tercera que es


puntero a long.

Dominio y operadores para los punteros


El dominio de una variable puntero es el de las direcciones de memoria.
En un PC las direcciones de memoria se codifican con 32 bits (4 bytes),
y toman valores desde 0 hasta FFFFFFFF, en base hexadecimal.

El operador sizeof, aplicado a cualquier variable de tipo puntero,


devuelve el valor 4.

Una observación importante: si un PC tiene direcciones de 32 bytes…


¿cuánta memoria puede llegar a direccionar?: Pues con 32 bits es
posible codificar hasta 232 bytes, es decir, hasta 4 ⋅ 230 bytes, es decir 4
Giga bytes de memoria.

Pero sigamos con los punteros. Ya tenemos el dominio. Codificará, en un


formato similar al de los enteros largos sin signo, las direcciones de toda
nuestra memoria. Ese será su dominio de valores.

Los operadores son los siguientes:

Operador dirección (&): Este operador se aplica a cualquier variable,


y devuelve la dirección de memoria de esa variable. Por ejemplo, se
puede escribir:
long x, *px;
px = &x;

205
Fundamentos de informática. Programación en Lenguaje C

Y así se ha creado una variable puntero a long llamada px, que servirá
para almacenar direcciones de variables de tipo long. Mediante la
segunda instrucción asignamos a ese puntero la dirección de la variable
x. Habitualmente se dice que px apunta a x.

El operador dirección no es propio de los punteros, sino de todas las


variables. Pero no hemos querido presentarlo hasta el momento en que
por ser necesario creemos que también ha de ser fácilmente
comprendido. De hecho este operador ya lo usábamos, por ejemplo, en
la función scanf, cuando se le indica a esa función “dónde” queremos
que almacene el dato que introducirá el usuario por teclado: por eso, en
esa función precedíamos el nombre de la variable cuyo valor se iba a
recibir por teclado con el operador &.

Hay una excepción en el uso de este operador: no puede aplicarse sobre


una variable que haya sido declarada register. El motivo es claro: al ser
una variable register, le hemos indicado al compilador que no almacene
su información en la memoria sino en un registro de la ALU. Y si la
variable no está en memoria, no tiene sentido que le solicitemos la
dirección de donde no está.

Operador indirección (*): Este operador sólo se aplica a los punteros.


Al aplicar a un puntero el operador indirección, se obtiene el contenido
de la posición de memoria “apuntada” por el puntero. Supongamos:
float pi = 3.14, *pt;
pt = &pi;

Con la primera instrucción, se han creado dos variables:

<pi, float, R1 , 3.14> y <pt, float*, R2 , ¿? >

Con la segunda instrucción damos valor a la variable puntero:

<pt, float*, R2 , R1 >

Ahora la variable puntero pt vale R1 , que es la dirección de memoria de


la variable pi.

206
Capítulo 8. Punteros.

Hablando ahora de la variable pt, podemos hacernos tres preguntas,


todas ellas referidas a direcciones de memoria.

1. ¿Dónde está pt? Porque pt es una variable, y por tanto está ubicada
en la memoria y tendrá una dirección. Para ver esa dirección, basta
aplicar a pt el operador dirección &. pt está en &pt.

2. ¿Qué vale pt? Y como pt es un puntero, pt vale o codifica una


determinada posición de memoria. Su valor pertenece al dominio de
direcciones y está codificado mediante 4 bytes. En concreto, pt vale
la dirección de la variable pi. pt vale &pi.

3. ¿Qué valor está almacenado en esa dirección de memoria a donde


apunta pt? Esta es una pregunta muy interesante, donde se muestra
la gran utilidad que tienen los punteros. Podemos llegar al valor de
cualquier variable tanto si disponemos de su nombre como si
disponemos de su dirección. Podemos llegar al valor de la posición
de memoria apuntada por pt, que como es un puntero a float, desde
el puntero tomará ese byte y los tres siguientes como el lugar donde
se aloja una variable float. Y para llegar a ese valor, disponemos del
operador indirección. El valor codificado en la posición almacenada
en pt es el contenido de pi: *pt es 3.14.

Al emplear punteros hay un peligro de confusión, que desconcierta al


principiante: al hablar de la dirección del puntero es fácil no entender si
nos referimos a la dirección que trae codificada en sus cuatro bytes, o la
posición de memoria dónde están esos cuatro bytes del puntero que
codifican direcciones.

Es muy importante que las variables puntero estén correctamente


direccionadas. Trabajar con punteros a los que no se les ha asignado
una dirección concreta conocida (la dirección de una variable) es muy
peligroso. En el caso anterior de la variable pi, se puede escribir:

*pt = 3.141596;

207
Fundamentos de informática. Programación en Lenguaje C

y así se ha cambiado el valor de la variable pi, que ahora tiene algunos


decimales más de precisión. Pero si la variable pt no estuviera
correctamente direccionada mediante una asignación previa… ¿en qué
zona de la memoria se hubiera escrito ese número 3.141596? Pues en la
posición que, por defecto, hubiera tenido esos cuatro bytes que codifican
el valor de la variable pt: quizá una dirección de otra variable, o a mitad
entre una variable y otra; o en un espacio de memoria no destinado a
almacenar datos, sino instrucciones, o el código del sistema operativo,…
En general, las consecuencias de usar punteros no inicializados, son
catastróficas para la buena marcha de un ordenador. Detrás de un
programa que “cuelga” al ordenador, muchas veces hay un puntero no
direccionado.

Pero no sólo hay que inicializar las variables puntero: hay que
inicializarlas bien, con coherencia. No se puede asignar a un puntero a
un tipo de dato concreto la dirección de una variable de un tipo de dato
diferente. Por ejemplo:
float x, px;
long y;
px = &y;

Si ahora hacemos referencia a *px… ¿trabajaremos la información de la


variable y como long, o como float? Y peor todavía:
float x, px;
char y;
px = &y;

Al hacer referencia a *px… ¿leemos la información del byte cuya


dirección es la de la variable y, o también se va a tomar en
consideración los otros tres bytes consecutivos? Porque la variable px
considera que apunta a variables de 4 bytes, que pasa eso es un
puntero a float. Pero la posición que le hemos asignado es la de una
variable tipo char, que únicamente ocupa un byte.

El error de asignar a un puntero la dirección de una variable de tipo de


dato distinto al puntero está, de hecho, impedido por el compilador, y si

208
Capítulo 8. Punteros.

encuentra una asignación de esas características, aborta el proceso de


compilación.

Punteros y vectores
Los punteros sobre variables simples tienen una utilidad clara en las
funciones. Allí los veremos con detenimiento. Lo que queremos ver
ahora es el uso de punteros sobre arrays.

Un array, o vector, es una colección de variables, todas del mismo tipo,


y colocadas en memoria de forma consecutiva. Si creo una array de
cinco variables float, y el primero de los elementos queda reservado en
la posición de memoria FF54:AB10, entonces no cabe duda que el
segundo estará en FF54:AB14, y el tercero en FF54:AB18, y el cuarto en
FF54:AB1C y el último en FF54:AB20.

Supongamos la siguiente situación:


long a[10];
long *pa;
pa = &a[0];

Y con esto a la vista, pasamos ahora a presentar otros dos operadores,


muy usados con los punteros.

Operador incremento (++) y decremento (––): Estos operadores


no son nuevos, y ya los conocemos. Todas sus propiedades que se
vieron con los enteros siguen vigentes ahora con las direcciones de
memoria.

Desde luego, si el incremento es sobre el contenido de la variable


apuntada sobre el puntero (*pa)++;, no hay nada que añadir: se
incrementará el contenido de esa variable apuntada, de la misma forma
que si el operador estuviera aplicado sobre la variable apuntada misma:
es lo mismo que escribir a[0]++;.

Nos referimos a incrementar el valor del puntero. ¿Qué sentido tiene


incrementar en 1 una dirección de memoria? El sentido será el de acudir

209
Fundamentos de informática. Programación en Lenguaje C

al siguiente valor situado en la memoria. Y si el puntero es de tipo long,


y apunta a variables long, entonces lo que se espera cuando se
incremente un 1 ese puntero es que su valor codificado se incremente
en 4: porque 4 es el número de bytes que deberemos saltar para pasar
de apuntar a una variable long a pasar a apuntar a otra variable del
mismo tipo almacenada de forma consecutiva.

Es decir, que si pa contiene la dirección de a[0], entonces pa + 1 es la


dirección del elemento a[1], y pa + 9 es la dirección del elemento a[9].

Y en el siguiente ejemplo, tenemos:


long a[10], *pa;
short b[10], *pb;
pa = &a[0];
pb = &b[0];

a b
F6C3:9870 Ã pa pb Ä F6C3:9870
b[0]
F6C3:9871 F6C3:9871
a[0]
F6C3:9872 pb + 1 Ä F6C3:9872
b[1]
F6C3:9873 F6C3:9873
F6C3:9874 Ã pa + 1 pb + 2 Ä F6C3:9874
b[2]
F6C3:9875 F6C3:9875
a[1]
F6C3:9876 pb + 3 Ä F6C3:9876
b[3]
F6C3:9877 F6C3:9877
F6C3:9878 Ã pa + 2 pb + 4 Ä F6C3:9878
b[4]
F6C3:9879 F6C3:9879
a[2]
F6C3:987A pb + 5 Ä F6C3:987A
b[5]
F6C3:987B F6C3:987B
F6C3:987C Ã pa + 3 pb + 6 Ä F6C3:987C
… …
F6C3:987D F6C3:987D

Al ir aumentando el valor del puntero, nos vamos desplazando por los


distintos elementos del vector, de tal manera que hablar de a[0] es lo
mismo que hablar de *pa; y hablar de a[1] es lo mismo que hablar de
*(pa + 1); y, en general, hablar de a[i] es lo mismo que hablar de *(pa
+ i). Y lo mismo si comentamos el ejemplo de las variables de tipo
short.

210
Capítulo 8. Punteros.

La operatoria o aritmética de punteros tiene en cuenta el tamaño de las


variables que se recorren. En el siguiente programa, y en la salida que
ofrece por pantalla, se puede ver este comportamiento de los punteros.
Sea cual sea el tipo de dato del puntero y de la variable a la que apunta,
si calculo la resta entre dos punteros situados uno al primer elemento de
un array y el otro al último, esa diferencia será la misma, porque la
resta de direcciones indica cuántos elementos de este tipo hay (caben)
entre esas dos direcciones. En nuestro ejemplo, todas esas diferencias
valen 9. Pero si lo que se calcula es el número de bytes entre la última
posición (apuntada por el segundo puntero) y la primera (apuntada por
el primer puntero), entonces esa diferencia sí dependerá del tamaño de
la variable del array.
#include <stdio.h>
void main(void)
{
char c[10], *pc1, *pc2;
short h[10], *ph1, *ph2;
float f[10], *pf1, *pf2;
double d[10], *pd1, *pd2;
long double ld[10], *pld1, *pld2;

pc1 = &c[0]; pc2 = &c[9];


ph1 = &h[0]; ph2 = &h[9];
pf1 = &f[0]; pf2 = &f[9];
pd1 = &d[0]; pd2 = &d[9];
pld1 = &ld[0]; pld2 = &ld[9];

printf(" pc2(%p) - pc1(%p) = %hd\n",pc2,pc1,pc2 - pc1);


printf(" ph2(%p) - ph1(%p) = %hd\n",ph2,ph1,ph2 - ph1);
printf(" pf2(%p) - pf1(%p) = %hd\n",pf2,pf1,pf2 - pf1);
printf(" pd2(%p) - pd1(%p) = %hd\n",pd2,pd1,pd2 - pd1);
printf("pld2(%p) - pld1(%p) = %hd\n",pld2,pld1,pld2 - pld1);
printf("\n\n");
printf("(long)pc2-(long)pc1=%3ld\n",(long)pc2-(long)pc1);
printf("(long)ph2-(long)ph1=%3ld\n",(long)ph2-(long)ph1);
printf("(long)pf2-(long)pf1=%3ld\n",(long)pf2-(long)pf1);
printf("(long)pd2-(long)pd1=%3ld\n",(long)pd2-(long)pd1);
printf("(long)pld2-(long)pld1=%3ld\n",(long)pld2-(long)pld1);
}

Que ofrece, por pantalla, el siguiente resultado:


pc2(0012FF89) - pc1(0012FF80) = 9
ph2(0012FF62) - ph1(0012FF50) = 9

211
Fundamentos de informática. Programación en Lenguaje C

pf2(0012FF4C) - pf1(0012FF28) = 9
pd2(0012FF20) - pd1(0012FED8) = 9
pld2(0012FECE) - pld1(0012FE74) = 9

(long)pc2 - (long)pc1 = 9
(long)ph2 - (long)ph1 = 18
(long)pf2 - (long)pf1 = 36
(long)pd2 - (long)pd1 = 72
(long)pld2 - (long)pld1 = 90

Que hemos de interpretar bien y entender.

Repetimos: al calcular la diferencia entre el puntero que apunta al


noveno elemento de la matriz y el que apunta al elemento cero, en
todos los casos el resultado ha de ser 9: porque en la operatoria de
punteros, independientemente del tipo del puntero, lo que se obtiene es
el número de elementos que hay entre las dos posiciones de memoria
señaladas.

Al convertir las direcciones en valores tipo long, ya no estamos


calculando cuántas variables hay entre ambas direcciones, sino la
diferencia entre el valor que codifica la última posición del vector y el
valor que codifica la primera dirección. Y en ese caso, el valor será
mayor según sea mayor el número de bytes que emplee el tipo de dato
referenciado por el puntero. Si es un char, entre la posición última y la
primera hay, efectivamente, 9 elementos; y el número de bytes entre
esas dos direcciones también es 9. Si es un float, entre la posición
última y la primera hay, efectivamente y de nuevo, 9 elementos; pero
ahora el número de bytes entre esas dos direcciones es 36, porque cada
uno de los nueve elementos ocupa cuatro bytes de memoria.

Índices y operatoria de punteros


Se puede recorrer un vector, o una cadena de caracteres mediante
índices. Y también, de forma equivalente, mediante operatoria de
punteros.

212
Capítulo 8. Punteros.

Pero además, los arrays y cadenas tienen la siguiente propiedad: Si


declaramos ese array o cadena de la siguiente forma:

tipo nombre_array[dimensión];

El nombre del vector o cadena es nombre_array. Para hacer uso de cada


una de las variables, se utiliza el nombre del vector o cadena seguido,
entre corchetes, del índice del elemento al que se quiere hacer
referencia: nombre_array[índice].

Y ahora introducimos otra novedad: el nombre del vector o cadena


recoge la dirección de la cadena, es decir, la dirección del primer
elemento de la cadena: decir nombre_array es lo mismo que decir
&nombre_array[0].

Y por tanto, y volviendo al código anteriormente visto:


long a[10], *pa;
short b[10], *pb;
pa = &a[0];
pb = &b[0];

Tenemos que *(pa + i) es lo mismo que a[i]. Y como decir a es


equivalente a decir &a[0] entonces, decir pa = &a[0] es lo mismo que
decir pa = a, y trabajar con el valor *(pa + i) es lo mismo que trabajar
con el valor *(a + i).

Y si podemos considerar que dar el nombre de un vector es equivalente


a dar la dirección del primer elemento, entonces podemos considerar
que ese nombre funciona como un puntero constante, con quien se
pueden hacer operaciones y formar parte de expresiones, mientras no
se le coloque en la parte Lvalue de un operador asignación.

Y muchos programadores, en lugar de trabajar con índices, recorren


todos sus vectores y cadenas mediante la operatoria o aritmética de
punteros.

Veamos un programa sencillo, resuelto mediante índices de vectores y


mediante la operatoria de punteros. Por ejemplo, un programa que
solicite al usuario una cadena de caracteres y luego la copie en otra

213
Fundamentos de informática. Programación en Lenguaje C

cadena en orden inverso: primero el último carácter, luego el penúltimo,


etc.

Con índices:
#include <stdio.h>
#include <string.h>
void main(void)
{
char orig[100], copia[100];
short i, l;
printf("Introduzca la cadena ... \n");
gets(orig);
l = strlen(orig);
for(i = 0 ; i < l ; i++)
copia[l - i - 1] = orig[i];
copia[i] = NULL;
printf("Cadena original: %s\n",orig);
printf("Cadena copia: %s\n",copia);
}

Con operatoria de punteros:


#include <stdio.h>
#include <string.h>
void main(void)
{
char orig[100], copia[100];
short i, l;

printf("Introduzca la cadena ... \n");


gets(orig);
l = strlen(orig);
for(i = 0 ; i < l ; i++)
*(copia + l - i - 1) = *(orig + i);
*(copia + i) = NULL;
printf("Cadena original: %s\n",orig);
printf("Cadena copia: %s\n",copia);
}

Desde luego, ambas formas de referirse a los distintos elementos del


vector son válidas.

En el capítulo en que hemos presentado los arrays hemos indicado que


es competencia del programador no recorrer el vector más allá de las
posiciones reservadas. Si se llega, mediante operatoria de índices o
mediante operatoria de punteros a una posición de memoria que no
pertenece realmente al vector, el compilador no detectará error alguno,

214
Capítulo 8. Punteros.

e incluso puede que tampoco se produzca un error en tiempo de


ejecución, pero estaremos accediendo a zona de memoria que quizá se
emplea para almacenar otra información. Y entonces alteraremos esos
datos de forma inconsiderada, con las consecuencias desastrosas que
eso pueda llegar a tener para el buen fin del proceso. Cuando en un
programa se llega equivocadamente, mediante operatoria de punteros o
de índices, más allá de la zona de memoria reservada, se dice que se ha
producido o se ha incurrido en una violación de memoria.

Puntero a puntero
Un puntero es una variable que contiene la dirección de otra variable.
Según sea el tipo de variable que va a ser apuntada, así, de ese tipo,
debe ser declarado el puntero. Ya lo hemos dicho.

Pero un puntero es también una variable. Y como variable que es, ocupa
una porción de memoria: tiene una dirección.

Se puede, por tanto, crear una variable que almacene la dirección de


esa variable puntero. Sería un puntero que almacenaría direcciones de
tipo de dato puntero. Un puntero a puntero.

Por ejemplo:

float F, *pF, **ppF;

Acabamos de crear tres variables: una, de tipo float, llamada F. Una


segunda variable, de tipo puntero a float, llamada pF. Y una tercera
variable, de tipo puntero a puntero float, llamada ppF.

Y eso no es un rizo absurdo. Tiene mucha aplicación en C. Igual que se


puede hablar de un puntero a puntero a puntero… a puntero a float.

Y así como antes hemos visto que hay una relación directa entre
punteros a un tipo de dato y vectores de este tipo de dato, también
veremos ahora que hay una relación directa entre punteros a punteros y
matrices de dimensión 2. Y entre punteros a punteros a punteros y

215
Fundamentos de informática. Programación en Lenguaje C

matrices de dimensión 3. Y si es conveniente trabajar con matrices de


dimensión n , entonces también lo es trabajar con punteros a punteros a
punteros…

Veámoslo con un ejemplo. Supongamos que creamos una matriz de


dimensión 2:

double m[4][6];

Antes hemos dicho que al crea un array, al hacer referencia a su nombre


estamos indicando la dirección del primero de sus elementos. Ahora, al
crear esta matriz, la dirección del elemento m[0][0] la obtenemos con el
nombre de la matriz: Es equivalente decir m que decir &m[0][0].

Pero la estructura que se crea al declarar una matriz es algo más


compleja que una lista de posiciones de memoria. En el ejemplo
expuesto de la matriz double, se puede considerar que se han creado
cuatro vectores de seis elementos cada uno y colocados en la memoria
uno detrás del otro de forma consecutiva. Y cada uno de esos vectores
tiene, como todo vector, la posibilidad de ofrecer la dirección de su
primer elemento. El cuadro 8.1. presenta un esquema de esta
construcción. Desde luego, no existen los punteros m, ni ninguno de los
*(m + i). Pero si empleamos el nombre de la matriz de esta forma,
entonces trabajamos con sintaxis de punteros.

De hecho, si ejecutamos el siguiente programa:


#include <stdio.h>
void main(void)
{
double m[4][6];
short i;
printf("m = %p\n",m);
for(i = 0 ; i < 4 ; i++)
{
printf("*(m + %hd) = %p\t",i, *(m + i));
printf("&m[%hd][0] = %p\n",i, &m[i][0]);
}
}

Obtenemos la siguiente salida:

216
Capítulo 8. Punteros.

m = 0012FECC
*(m + 0) = 0012FECC &m[0][0] = 0012FECC
*(m + 1) = 0012FEFC &m[1][0] = 0012FEFC
*(m + 2) = 0012FF2C &m[2][0] = 0012FF2C
*(m + 3) = 0012FF5C &m[3][0] = 0012FF5C

m Ä *(m+0) Ä m[0][0] m[0][1] m[0][2] m[0][3] m[0][4] m[0][5]


*(m+1) Ä m[1][0] m[1][1] m[1][2] m[1][3] m[1][4] m[1][5]
*(m+2) Ä m[2][0] m[2][1] m[2][2] m[2][3] m[2][4] m[2][5]
*(m+3) Ä m[3][0] m[3][1] m[3][2] m[3][3] m[3][4] m[3][5]

Cuadro 8.1.: Distribución de la memoria en la matriz


double m[4][6];

Tenemos que m vale lo mismo que *(m + 0); su valor es la dirección del
primer elemento de la matriz: m[0][0]. Después de él, vienen todos los
demás, uno detrás de otro: después de m[0][5] vendrá el m[1][0], y
esa dirección la podemos obtener con *(m + 1); después de m[1][5]
vendrá el m[2][0], y esa dirección la podemos obtener con *(m + 2);
después de m[2][5] vendrá el m[3][0], y esa dirección la podemos
obtener con *(m + 3); y después de m[3][5] se termina la cadena de
elementos reservados.

Es decir, en la memoria del ordenador, no se distingue entre un vector


de 24 variables tipo double y una matriz 4 * 6 de variables tipo
double. Es el lenguaje el que sabe interpretar, mediante una operatoria
de punteros, una estructura matricial donde sólo se dispone de una
secuencia lineal de elementos. Esos punteros no existen en realidad, y si
se imprime la posición que ocupa m, ó *m la pantalla nos mostrará el
mismo valor que al solicitarle que nos muestre la dirección de m[0][0].
No existen, pero el lenguaje C admite esa sintaxis, y podemos trabajar
como si de punteros constantes se trataran.

217
Fundamentos de informática. Programación en Lenguaje C

Y así como antes hemos podido trabajar un programa con un array


mediante operatoria de punteros, ahora vamos a hacer lo mismo con un
programa que emplee matrices.

Veamos un programa que calcula el determinante de una matriz de tres


por tres.

Con índices:
#include <stdio.h>
void main(void)
{
double m[3][3];
double det;
short i,j;
for(i = 0 ; i < 3 ; i++)
for(j = 0 ; j < 3 ; j++)
{
printf("m[%hd][%hd] = ", i, j);
scanf("%lf",&m[i][j]);
}
det = 0;
det += (m[0][0] * m[1][1] * m[2][2]);
det += (m[0][1] * m[1][2] * m[2][0]);
det += (m[0][2] * m[1][0] * m[2][1]);
det -= (m[0][2] * m[1][1] * m[2][0]);
det -= (m[0][1] * m[1][0] * m[2][2]);
det -= (m[0][0] * m[1][2] * m[2][1]);
printf("El determinante ... \n");
for(i = 0 ; i < 3 ; i++)
{
printf("\n | ");
for(j = 0 ; j < 3 ; j++)
printf("%8.2lf",m[i][j]);
printf(" | ");
}
printf("\n\n es ... %lf",det);
}

Con operatoria de punteros:


#include <stdio.h>
void main(void)
{
double m[3][3];
double det;
short i,j;
for(i = 0 ; i < 3 ; i++)
for(j = 0 ; j < 3 ; j++)
{

218
Capítulo 8. Punteros.

printf("m[%hd][%hd] = ", i, j);


scanf("%lf",*(m + i) + j);
}
det = 0;
det += *(*(m + 0) + 0) * *(*(m + 1) + 1) * *(*(m + 2) + 2);
det += *(*(m + 0) + 1) * *(*(m + 1) + 2) * *(*(m + 2) + 0);
det += *(*(m + 0) + 2) * *(*(m + 1) + 0) * *(*(m + 2) + 1);
det -= *(*(m + 0) + 2) * *(*(m + 1) + 1) * *(*(m + 2) + 0);
det -= *(*(m + 0) + 1) * *(*(m + 1) + 0) * *(*(m + 2) + 2);
det -= *(*(m + 0) + 0) * *(*(m + 1) + 2) * *(*(m + 2) + 1);
printf("El determinante ... \n");
for(i = 0 ; i < 3 ; i++)
{
printf("\n | ");
for(j = 0 ; j < 3 ; j++)
printf("%8.2lf",*(*(m + i) + j));
printf(" | ");
}
printf("\n\n es ... %lf",det);

Desde luego, la operatoria de punteros con matrices resulta algo


farragosa en un primer momento. Pero no encierra dificultad de
concepto.

Advertencia final
El uso de puntero condiciona el modo de programar. El puntero es una
herramienta muy poderosa y muy arriesgada también. Es necesario
saber bien qué se hace, porque jugando con punteros se pueden
“burlar” muchas seguridades de C.

Veamos un ejemplo. Primero presentamos el siguiente código, donde se


emplea una variable static: una variable que se extiende a la duración
de todo el programa, pero cuyo ámbito queda reducido al del bloque en
el que se ha definido. Esa variable, que en el ejemplo llamamos local, se
inicializa a cero la primera vez que se entra en el bloque donde está
definida, y posteriormente se va aumentando de uno en uno cada vez
que se ejecuta su bloque.
#include <stdio.h>
void main(void)
{
short a, b = 0;

219
Fundamentos de informática. Programación en Lenguaje C

do
{
a = 0;
do
{
static short local = 0;
local++;
printf("local = %2hd\n",local);
a++;
}while (a < 5);
printf("\n");
b++;
}while(b < 5);
}

La salida de este programa es la que sigue:


local = 1 local = 2 local = 3 local = 4 local = 5
local = 6 local = 7 local = 8 local = 9 local = 10
local = 11 local = 12 local = 13 local = 14 local = 15
local = 16 local = 17 local = 18 local = 19 local = 20
local = 21 local = 22 local = 23 local = 24 local = 25

Cada vez que se entra en el ámbito de la variable local, se vuelve a


poder trabajar sobre ella y se sigue incrementando desde el valor en
que quedo después de la última modificación.

Pero si ahora introducimos en el programa los siguientes cambios:


#include <stdio.h>
void main(void)
{
short a, b = 0;
short *c;
do
{
a = 0;
do
{
static short local = 0;
c = &local;
local++;
printf("local = %hd\t",local);
a++;
}while (a < 5);
printf("\n");
b++;
*c = 0;
}while(b < 5);

Entonces la salida es:

220
Capítulo 8. Punteros.

local = 1 local = 2 local = 3 local = 4 local = 5


local = 1 local = 2 local = 3 local = 4 local = 5
local = 1 local = 2 local = 3 local = 4 local = 5
local = 1 local = 2 local = 3 local = 4 local = 5
local = 1 local = 2 local = 3 local = 4 local = 5

Y es que con el puntero, y desde fuera del ámbito de la variable local, le


hemos cambiado su valor y la hemos inicializado a cero una y otra vez.
Y eso es peligroso, porque podemos violar las reglas de validez de las
variables. Es mejor evitar operaciones de este estilo. No se debe jugar
en contra de las reglas de la sintaxis del lenguaje C. Es mejor programar
de acuerdo con esas reglas.

Ejercicios

52. Leer el siguiente código y completar la salida que ofrece por


pantalla: (No está resuelto).

#include <stdio.h>
void main(void)
{
char c[3];
short i[3], cont;
float f[3];
printf("Las direcciones de memoria son:\n");
for(cont = 0 ; cont < 3 ; cont++)
{
printf("&c[%2d] = %10p\t",cont,c + cont);
printf("&i[%2d] = %10p\t",cont,i + cont);
printf("&f[%2d] = %10p\n\n",cont,f + cont);
}
}

Las direcciones de memoria son:

&c[ 0] = 0064FE01 &i[ 0] = 0064FDF8 &f[ 0] = 0064FDEC

&c[ 1] = &i[ 1] = &f[ 1] =

&c[ 2] = &i[ 2] = &f[ 2] =

221
Fundamentos de informática. Programación en Lenguaje C

53. Leer el siguiente código y completar la salida que ofrece por


pantalla.

#include <stdio.h>
main()
{
char c[20],*pc1,*pc2;
short int i[20],*pi1,*pi2;
float f[20],*pf1,*pf2;

pc1 = &c[0];
pc2 = &c[19];
pi1 = &i[0];
pi2 = &i[19];
pf1 = &f[0];
pf2 = &f[19];

printf("(int)pc2-(int)pc1 es %d\n",(int)pc2-(int)pc1);
printf("pc2 - pc1 es %d\n\n",pc2 - pc1);

printf("(int)pi2-(int)pi1 es %d\n",(int)pi2-(int)pi1);
printf("pi2 - pi1 es %d\n\n",pi2 - pi1);

printf("(int)pf2-(int)pf1 es %d\n",(int)pf2-(int)pf1);
printf("pf2 - pf1 es %d\n\n",pf2 - pf1);
}

(int)pc2 - (int)pc1 es 19
pc2 - pc1 es 19
(int)pi2 - (int)pi1 es 38
pi2 - pi1 es 19
(int)pf2 - (int)pf1 es 76
pf2 - pf1 es 19

En general una buena forma de aprendere a manejar punteros es


intentar rehacer todos los ejercicios ya resueltos en los dos capítulos
anteriores empleando ahora operatoria de punteros y recorriendo los
vectores y matrices mediante la indirección.

222
CAPÍTULO 9
FUNCIONES

Hemos llegado a las funciones.

Al principio del tema de estructuras de control señalábamos que había


dos maneras de romper el flujo secuencial de sentencias. Y hablábamos
de dos tipos de instrucciones que rompen el flujo: las instrucciones
condicionales y de las incondicionales. Las primeras nos dieron pie a
hablar largamente de las estructuras de control: condicionales y de
iteración. Ahora toca hablar de las instrucciones incondicionales que
realizan la transferencia a una nueva dirección del programa sin evaluar
condición alguna.

De hecho, ya hemos visto muchas funciones. Y las hemos utilizado.


Cuando hemos querido mostrar por pantalla un mensaje hemos acudido
a la función printf de la biblioteca stdio.h. Cuando hemos querido saber
la longitud de una cadena hemos utilizado la función strlen, de la
biblioteca string.h. Y cuando hemos querido hallar la función seno de
un valor concreto, hemos acudido a la función sin, de math.h.
Fundamentos de informática. Programación en Lenguaje C

Y ya hemos visto que, sin saber cómo, hemos echado mano de una
función estándar programada por ANSI C que nos ha resuelto nuestro
problema. ¿Cómo se ha logrado que se vea en pantalla un texto, o el
valor de una variable? ¿Qué desarrollo de Taylor se ha aplicado para
llegar a calcular el seno de un ángulo dado? No lo sabemos. ¿Dónde está
el código que resuelve nuestro problema? Tampoco lo sabemos. Pero
cada vez que hemos invocado a una de esas funciones, lo que ha
ocurrido es que el contador de programa ha abandonado nuestra
secuencia de sentencias y se ha puesto con otras sentencias, que son
las que codifican las funciones que hemos invocado.

De forma incondicional, cada vez que se invoca una función, se


transfiere el control de ejecución a otra dirección de memoria, donde se
encuentran codificadas otras sentencias, que resuelven el problema para
el que se ha definido, editado y compilado esa función.

Son transferencias de control con retorno. Porque cuando termina la


ejecución de la última de las sentencias de la función, entonces regresa
el control a la sentencia inmediatamente posterior a aquella que invocó
esa función.

Quizá ahora, cuando vamos a abordar la teoría de creación, diseño e


implementación de funciones, será buen momento para releer lo que
decíamos en el tema 4 al tratar de la modularidad. Y recordar también
las tres propiedades que debían tener los distintos módulos:
independencia funcional, comprensibilidad, adaptabilidad. No lo vamos a
repetir ahora: allí se trató.

Definiciones
Abstracción – Modularidad – Programación estructurada.

Esas eran las tres notas básicas que presentamos al presentar el


lenguaje de programación C. De la programación estructurada ya hemos
hablado, y lo seguiremos haciendo en este capítulo. La abstracción es el

224
Capítulo 9. Funciones.

paso previo de toda programación: conocer el sistema e identificar los


más significativos elementos que dan con la esencia del problema a
resolver. Y la modularidad es la capacidad de dividir el sistema
estudiado en partes diferenciadas.

Eso que hemos llamado módulo, en un lenguaje de programación se


puede llamar procedimiento o se puede llamar función. Las funciones y
los procedimientos permiten crear programas complejos, mediante un
reparto de tareas que permite construir el programa de forma
estructurada y modular.

Desde un punto de vista académico, se entiende por procedimiento el


conjunto de sentencias a las que se asocia un identificador (un nombre),
y que realiza una tarea que se conoce por los cambios que ejerce sobre
el conjunto de variables. Y entendemos por función el conjunto de
sentencias a las que se asocia un identificador (un nombre) y que
genera un valor nuevo, calculado a partir de los argumentos que recibe.

Los elementos que componen un procedimiento o función son, pues:

1. Un identificador, que es el nombre que sirve para invocar a esa


función o a ese procedimiento.

2. Una lista de parámetros, que es el conjunto de variables que se


facilitan al procedimiento o función para que realice su tarea
modularizada. Al hacer la abstracción del sistema, y modularlo en
partes más accesibles, hay que especificar los parámetros formales
que permiten la comunicación y definen el dominio (tipo de dato) de
los datos de entrada. Esa lista de parámetros define el modo en que
podrán comunicarse el programa que utiliza a la función y la función
usada.

3. Un cuerpo o conjunto de sentencias. Las necesarias para poder


realizar la tarea para la que ha sido definida la función o el
procedimiento.

225
Fundamentos de informática. Programación en Lenguaje C

4. Un entorno. Entendemos por entorno el conjunto de variables


globales, y externas por tanto al procedimiento o función, que
pueden ser usadas y modificadas dentro del ámbito de la función.
Esas variables, por ser globales y por tanto definidas en un ámbito
más amplio al ámbito local de la función, no necesitan ser
explicitadas en la lista de parámetros de la función.

Es una práctica desaconsejable trabajar con el entorno de la función


desde el ámbito local de la función. Hacerlo lleva consigo que esa
función deja de ser independiente de ese entorno y, por tanto, deja
de ser exportable. Perderíamos entonces el valor de la
independencia funcional, que es una de las propiedades de la
programación por módulos.

Podemos pues concluir que el uso de variables globales dentro del


cuerpo de un procedimiento o función es altamente desaconsejable.

En el lenguaje C no se habla habitualmente de procedimientos, sino sólo


de funciones. Pero de hecho existen de las dos cosas. Procedimientos
serían, por ejemplo, la función printf no se invoca para calcular valores
nuevos, sino para realizar una tarea sobre las variables. Más claro se ve
con la función scanf que, efectivamente, realiza una tarea que se conoce
por los cambios que ejerce sobre una variable concreta. Y funciones
serían, por ejemplo, la función strlen, que a partir de una cadena de
caracteres que recibe como parámetro de entrada calcula un valor, que
es la longitud de esa cadena; o la función sin, que a partir de un ángulo
que recibe como valor de entrada, calcula el seno de ese ángulo como
valor de salida.

En definitiva, una función es una porción de código, identificada con un


nombre concreto (su identificador), que realiza una tarea concreta, que
puede ser entendida de forma independiente al resto del programa, y
que tiene muy bien determinado cómo se hace uso de ella, con qué
parámetros se la invoca y bajo qué condiciones puede ser usada, cuál es
la tarea que lleva a cabo, y cuál es el valor que calcula y devuelve.

226
Capítulo 9. Funciones.

Tanto los procedimientos como las funciones pueden ser vistos como
cajas negras: un código del que desconocemos sus sentencias, al que se
le puede suministrar unos datos de entrada y obtener modificaciones
para esos valores de entrada y/o el cálculo de un nuevo valor, deducido
a partir de los valores que ha recibido como entrada.

Con eso se consigue programas más cortos; que el código pueda ser
usado más de una vez; mayor facilidad para gestionar un correcto orden
de ejecución de sentencias; que las variables tengan mayor carácter
local, y no puedan ser manipuladas fuera del ámbito para el que han
sido creadas.

Funciones en C
Una función, en C, es un segmento independiente de código fuente,
diseñado para realizar una tarea específica.

Las funciones son los elementos principales de un programa en C. Cada


una de las funciones de un programa constituye una unidad, capaz de
realizar una tarea determinada. Quizá se podría decir que un programa
es simplemente un conjunto de definiciones de distintas funciones,
empleadas luego de forma estructurada.

La primera función que aparece en todo programa C es la función


principal, o función main. Todo programa ejecutable tiene una, y sólo
una, función main. Un programa sin función principal no genera un
ejecutable. Y si todas las funciones se crean para poder ser utilizadas, la
función principal es la única que no puede ser usada por nadie: nadie
puede invocar a la función principal de un programa. Tampoco puede
llamarse a sí misma (aunque este concepto de “autollamada”,
denominado recurrencia, lo trataremos más adelante).

Además de la función principal, en un programa se pueden encontrar


otras funciones: o funciones creadas y diseñadas por el programador
para esa aplicación, o funciones ya creadas e implementadas y

227
Fundamentos de informática. Programación en Lenguaje C

compiladas en librerías: de creación propia o adquirida o pertenecientes


al estándar de ANSI C.

Las funciones estándar de ANSI C se encuentran clasificadas en distintas


librerías de acuerdo con las tareas que desarrollan. Al montar un
programa en C, se buscan en las librerías las funciones que se van a
necesitar, que se incluyen en el programa y se hacen así parte del
mismo.

También se pueden crear las propias funciones en C. Así, una vez


creadas y definidas, ya pueden ser invocadas tantas veces como se
quiera. Y así, podemos ir creando nuestras propias bibliotecas de
funciones.

Siempre que hemos hablado de funciones hemos utilizado dos verbos,


uno después del otro: creación y definición de la función. Y es que en
una función hay que distinguir entre su declaración o prototipo (creación
de la función), su definición (el cuerpo de código que recoge las
sentencias que debe ejecutar la función para lograr llevar a cabo su
tarea) y, finalmente, su invocación o llamada: una función creada y
definida sólo se ejecuta si otra función la invoca o llama. Y en definitiva,
como la única función que se ejecuta sin ser invocada (y también la
única función que no permite ser invocada) es la función main,
cualquier función será ejecutada únicamente si es invocada por la
función main o por alguna función que ha sido invocada por la función
main o tiene en su origen, en una cadena de invocación, una llamada
desde la función main.

Declaración de la función.
La declaración de una función se realiza a través de su prototipo. Un
prototipo tiene la forma:

tipo_funcion nombre_funcion([tipo1 [var1] [,… tipoN [varN]]);

228
Capítulo 9. Funciones.

Donde tipo_funcion declara de qué tipo es el valor que devolverá la


función. Una función puede devolver valores de cualquier tipo de dato
válido en C, tanto primitivo como diseñado por el programador (se verá
la forma de crear tipos de datos en unos temas más adelante). Si no
devuelve ningún valor, entonces se indica que es de tipo void.

Donde tipo1,…, tipoN declara de qué tipo es cada uno de los valores
que la función recibirá como parámetros al ser invocada. En la
declaración del prototipo es opcional indicar el nombre que tomarán las
variables que recibirán esos valores y que se comportarán como
variables locales de la función. Sea como sea, ese nombre sí deberá
quedar recogido en la definición de la función. Pero eso es adelantar
acontecimientos.

Al final de la declaración viene el punto y coma. Y es que la declaración


de una función es una sentencia en C. Una sentencia que se consigna
fuera de cualquier función. La declaración de una función tiene carácter
global dentro de programa donde se declara. No se puede declarar, ni
definir, una función dentro de otra función: eso siempre dará error de
compilación.

Toda función que quiera ser definida e invocada debe haber sido
previamente declarada. El prototipo de la función presenta el modo en
que esa función debe ser empleada. Es como la definición de su
interface, de su forma de comunicación: qué valores, de qué tipo y en
qué orden debe recibir la función como argumentos al ser invocada. El
prototipo permite localizar cualquier conversión ilegal de tipos entre los
argumentos utilizados en la llamada de la función y los tipos definidos
en los parámetros, entre los paréntesis del prototipo. Además, controla
que el número de argumentos usados en una llamada a una función
coincida con el número de parámetros de la definición.

Existe una excepción a esa regla: cuando una función es de tipo int,
puede omitirse su declaración. Pero es recomendable no hacer uso de
esa excepción. Si en una expresión, en una sentencia dentro del cuerpo

229
Fundamentos de informática. Programación en Lenguaje C

de una función, aparece un nombre o identificador que no ha sido


declarado previamente, y ese nombre va seguido de un paréntesis de
apertura, el compilador supone que ese identificador corresponde al
nombre de una función de tipo int.

Todas las declaraciones de función deben preceder a la definición del


cuerpo de la función main.

Definición de la función.
Ya tenemos la función declarada. Con el prototipo ha quedado definido
el modo en que podemos utilizarla: cómo nos comunicamos nosotros
con ella y qué resultado nos ofrece.

Ahora queda la tarea de definirla.

Hay que escribir el código, las sentencias, que van a realizar la tarea
para la que ha sido creada la función.

La forma habitual que tendrá la definición de una función la conocemos


ya, pues hemos visto ya muchas: cada vez que hacíamos un programa,
y escribíamos la función principal, estábamos definiendo esa función
main. Esa forma es:
tipo_funcion nombre_funcion([tipo1 var1][,… tipoN varN])
{
[declaración de variables locales]
[cuerpo de la función: grupo de sentencias]
[return(parámetro);]
}

Donde el tipo_función debe coincidir con el de la declaración, lo mismo


que nombre_función y lo mismo que la lista de parámetros. Ahora, en la
definición, los parámetros de la función siguen recogiendo el tipo de
dato y el nombre de la variable: pero ahora ese nombre NO es opcional.
Debe ponerse, porque esos nombres serán los identificadores de las
variables que recojan los valores que se le pasan a la función cuando se
la llama o invoca. A esas variables se las llama parámetros formales:

230
Capítulo 9. Funciones.

son variables locales a la función: se crean cuando la función es


invocada y se destruyen cuando se termina la ejecución de la función.

La lista de parámetros puede ser una lista vacía porque no se le quiera


pasar ningún valor a la función: eso es frecuente. En ese caso, tanto en
el prototipo como en la definición, entre los paréntesis que siguen al
nombre de la función se coloca la palabra clave void.

tipo_función nombre_función(void); // declaración del prototipo

Si la función no devuelve valor alguno, entonces se indica como de tipo


void, al igual que ya se hizo en la definición del prototipo. Una función
declarada como de tipo void no puede ser usada como operando en una
expresión de C, porque esa función no tiene valor alguno. Una función
de tipo void puede mostrar datos por pantalla, escribir o leer ficheros,
etc.

El bloque de la función tiene tres partes: la declaración de las variables


locales, el cuerpo de la función, donde están las sentencias que llevarán
a cabo la tarea para la que ha sido creada y definida la función, y la
sentencia return, de la que hablaremos enseguida.

El bloque de la función viene recogido entre llaves. Aunque la función


tenga una sola sentencia, es obligatorio recoger esa sentencia única
entre las llaves de apertura y de cerrado.

Las variables creadas en el cuerpo de la función serán locales a ella. Se


pueden usar identificadores idénticos para nombrar distintas variables
de diferentes funciones, porque cada variable de cada función pertenece
a un ámbito completamente disjunto al ámbito de otra función, y no hay
posibilidad alguna de confusión. Cada variable tendrá su dirección y su
ámbito distintos.

Aunque ya se ha dicho anteriormente, recordamos que todas las


funciones en C, sin excepción alguna, están en el mismo nivel de
ámbito, es decir, no se puede declarar ninguna función dentro de otra

231
Fundamentos de informática. Programación en Lenguaje C

función, y no se puede definir una función como bloque interno en el


cuerpo de otra función.

Llamada a la función
La llamada a una función es una sentencia habitual en C. Ya la hemos
usado con frecuencia, invocando hasta el momento únicamente
funciones de biblioteca. Pero la forma de invocar es la misma para
cualquier función.

nombre_función([argumento1][, …, argumentoN]);

La sentencia de llamada está formada por el nombre de la función y sus


argumentos (los valores que se le pasan) que deben ir recogidos en el
mismo orden que la secuencia de parámetros del prototipo y entre
paréntesis. Si la función no recibe parámetros (porque así esté definida),
entonces se coloca después de su nombre los paréntesis de apertura y
cerrado sin ninguna información entre ellos. Si no se colocan los
paréntesis, se produce un error de compilación.

El paso de parámetros en la llamada exige una asignación para cada


parámetro. El valor del primer argumento introducido en la llamada a la
función queda asignado en la variable del primer parámetro formal de la
función; el segundo valor de argumento queda asignado en el segundo
parámetro formal de la función; y así sucesivamente. Hay que asegurar
que el tipo de dato de los parámetros formales es compatible en cada
caso con el tipo de dato usado en lista de argumentos en la llamada de
la función. El compilador de C no dará error si se fuerzan cambios de
tipo de dato incompatibles, pero el resultado será inesperado
totalmente.

La lista de argumentos estará formada por nombres de variables que


recogen los valores que se desean pasar, o por literales. No es necesario
(ni es lo habitual) que los identificadores de los argumentos que se

232
Capítulo 9. Funciones.

pasan a la función cuando es llamada coincidan con los identificadores


de los parámetros formales.

Las llamadas a las funciones, dentro de cualquier función, pueden


realizarse en el orden que sea necesario, y tantas veces como se quiera,
independientemente del orden en que hayan sido declaradas o definidas.
Incluso se da el caso, bastante frecuente como veremos más adelante,
que una función pueda llamarse a sí misma. Esa operación de
autollamada se llama recurrencia.

Si la función debe devolver un valor, con cierta frecuencia interesará


que la función que la invoca almacene ese valor en una variable local
suya. En ese caso, la llamada a la función será de la forma:

variable = nombre_función([argumento1][, …, argumentoN]);

Aunque eso no siempre se hace necesario, y también con frecuencia


encontraremos las llamadas a las funciones como partes de una
expresión.

La sentencia return
Hay dos formas ordinarias de terminar la ejecución de una función.

1. Llegar a la última sentencia del cuerpo, antes de la llave que cierra el


bloque de esa función.

2. Llegar a una sentencia return. La sentencia return fuerza la salida


de la función, y devuelve el control del programa a la función que la
llamó, en la sentencia inmediatamente posterior a la de la llamada a
la función.

Si la función es de un tipo de dato distinto de void, entonces en el


bloque de la función debe recogerse, al menos, una sentencia return.
En ese caso, además, en esa sentencia y a continuación de la palabra
return, deberá ir el valor que devuelve la función: o el identificador de

233
Fundamentos de informática. Programación en Lenguaje C

una variable o un literal, siempre del mismo tipo que el tipo de la


función o de otro tipo compatible.

Una función tipo void no necesariamente tendrá la sentencia return. En


ese caso, la ejecución de la función terminará con la sentencia última
del bloque. Si una función de tipo void hace uso de sentencias return,
entonces en ningún caso debe seguir a esa palabra valor alguno: si así
fuera, el compilador detectará un error y no compilará el programa.

La sentencia return puede encontrarse en cualquier momento del


código de una función. De todas formas, no tendría sentido recoger
ninguna sentencia más allá de una sentencia return que no estuviera
condicionada, pues esa sentencia jamás llegaría a ejecutarse.

En resumen, la sentencia return realiza básicamente dos operaciones:

1. Fuerza la salida inmediata del cuerpo de la función y se vuelve a la


siguiente sentencia después de la llamada.

2. Si la función no es tipo void, entonces además de terminar la


ejecución de la función, devuelve un valor a la función que la llamó.
Si esa función llamante no recoge ese valor en una variable, el valor
se pierde, con todas las variables locales de la función abandonada.

La forma general de la sentencia return es:

return [expresión];

Muchos programadores habitúan a colocar la expresión del return entre


paréntesis. Es opcional, como lo es en la redacción de cualquier
expresión.

Si el tipo de dato de la expresión del return no coincide con el tipo de la


función entonces, de forma automática, el tipo de dato de la expresión
se convierte en el tipo de dato de la función.

Ha llegado el momento de ver algunos ejemplos. Veamos primero una


función de tipo void: una que muestre un mensaje por pantalla.

234
Capítulo 9. Funciones.

Declaración: void mostrar(short);

Definición:
void mostrar(short x)
{
printf(“El valor recibido es %hd.”, x);
}

Llamada: mostrar(10);

que ofrece la siguiente salida por pantalla:

El valor recibido es 10.

Otro ejemplo: Una función que reciba un entero y devuelva el valor de


su cuadrado.

Declaración: unsigned long int cuadrado(short);

Definición:
unsigned long int cuadrado(short x)
{
return x * x;
}

Una posible llamada:

printf(“El cuadrado de %hd es %ld.\n”, a, cuadrado(a));

Un tercer ejemplo, ahora con dos sentencias return: una función que
reciba como parámetros formales dos valores enteros y devuelve el
valor del mayor de los dos:

Declaración: short mayor(short, short);

Definición:
short mayor(short x, short y)
{
if(x > y) return x;
else return y;
}

Desde luego la palabra else podría omitirse, porque jamás se llegará a


ella si se ejecuta el primer return, y si la condición del if es falsa,
entonces se ejecuta el segundo return.

235
Fundamentos de informática. Programación en Lenguaje C

Otra posible definición:


short mayor(short x, short y)
{
x > y ? return(x) : return(y);
}

Llamada:

A = mayor(a,b);

Donde la variable A guardará el mayor de los dos valores entre a y b.

Una última observación: el tipo de la función puede ser un tipo de dato


puntero. En ese caso el valor que devuelve la función será una dirección
de memoria donde se aloja un valor, o un vector, o una matriz. Más
adelante, en los siguientes capítulos, veremos algún ejemplo donde
este tipo de función resulta de gran utilidad.

Ámbito y vida de las variables


Ya conocemos el concepto de ámbito de la variable. Y ahora que ya
sabemos algo de las funciones, es conveniente presentar cuándo se
puede acceder a cada variable, cuándo diremos que está viva, etc.

Veamos un programa ya conocido, el del cálculo del factorial de un


entero, resuelto ahora mediante funciones:
#include <stdio.h>
long Factorial(short);
void main(void)
{
short n;
printf("Introduzca el valor de n ... ");
scanf("%hd", &n);
printf("El factorial de %hd ",n);
printf("es %ld",Factorial(n));
}

long Factorial(short a)
{
long F = 1;
while(a) F *= a--;
return F;
}

236
Capítulo 9. Funciones.

En este programa, la función principal main tiene definida una variable


de tipo short, a la que hemos llamado n. En esa función, esa variable es
local, y podemos recoger sus características en la cuádrupla:

< n, short , Rn , Vn >

La variable, de tipo short, n se almacena en la dirección de memoria Rn


y guardará el valor que reciba de la función scanf.

La función main invoca a la función Factorial. En la llamada se pasa


como parámetro el valor de la variable n. En esa llamada, el valor de la
variable n se copia en la variable a de Factorial:

< a, short , Ra , Vn >

Desde el momento en que se produce la llamada a la función Factorial,


abandonamos el ámbito de la función main. En este momento, la
variable n está fuera de ámbito y no puede, por tanto hacerse uso de
ella. No ha quedado eliminada: estamos en el ámbito de Factorial pero
aún no han terminado todas las sentencias de main. En el cálculo
dentro de la función Factorial se ve modificado el valor de la variable
local a. Pero esa modificación para nada influye en la variable n, que
está definida en otra posición de memoria distinta.

Cuando se termina la ejecución de la función Factorial, el control del


programa vuelve a la función main. La variable a y la variable F
mueren, pero el valor de la variable F ha sido recibido como parámetro
en la función printf, y así podemos mostrarlo por pantalla. Ahora, de
nuevo en la función principal, volvemos al ámbito de la variable n, de la
que podríamos haber hecho uso si hubiera sido necesario.

Veamos ahora otro ejemplo, con un programa que calcule el máximo


común divisor de dos enteros. De nuevo, resolvemos el problema
mediante funciones:
#include <stdio.h>
long euclides(long, long);
void main(void)
{

237
Fundamentos de informática. Programación en Lenguaje C

long n1, n2;


do
{
printf("Introduzca el valor de n1 ... ");
scanf("%ld", &n1);
printf("Introduzca el valor de n2 ... ");
scanf("%ld", &n2);
if(n2 != 0)
printf("\nEl mcd de %ld y %ld “,n1, n2);
printf(“es %ld\n", euclides(n1,n2));
}while(n2 != 0);
}

long euclides(long a, long b)


{
static short cont = 0;
long mcd;
while(b)
{
mcd = b;
b = a % b;
a = mcd;
}
printf("Invocaciones a la función ... %hd\n", ++cont);
return mcd;
}

En esta ocasión, además, hemos incluido una variable static en la


función euclides. Esta variable nos informará de cuántas veces se ha
ejecutado la función.

Las variables n1 y n2, de main, dejan de estar accesibles cuando se


invoca a la función euclides. En ese momento se copian sus valores en
las variables a y b que comienzan a existir precisamente en el momento
de la invocación de la función. Además de esas variables locales, y de la
variable local mcd, se ha creado otra, llamada cont, que es también
local a euclides pero que, a diferencia de las demás variables locales, no
desaparecerá cuando se ejecute la sentencia return y se devuelva el
control de la aplicación a la función main: es una variable declarada
static. Cuando eso ocurra, perderá la variable cont su ámbito, y no
podrá ser accedida, pero en cuanto se invoque de nuevo a la función
euclides, allí estará la variable, ya creada, accesible para cuando la
función la requiera.

238
Capítulo 9. Funciones.

Recurrencia
Ya lo hemos comentado antes. Una función decimos que es recurrente si
existe una llamada a sí misma en el cuerpo de su definición.

Conceptualmente la recurrencia no ofrece mucha complicación. El quid


de la recurrencia está en saber utilizarla. Hay problemas donde la
recurrencia cabe perfectamente, y agiliza mucho los algoritmos de
solución. Implementar una función mediante recurrencia es sencillo en
C. El problema no está en el C, sino en llegar al algoritmo que haga uso
de ella.

Lo más adecuado para explicar la recurrencia es ver algunos ejemplos.


Veremos la solución de los dos programas del epígrafe anterior,
solventados ahora mediante recurrencia. Comenzamos por el cálculo del
factorial.

Por definición, el factorial de un entero positivo es igual al producto de


ese entero por el factorial del entero inmediatamente inferior:

n ! = n ⋅ (n − 1)!

Esta forma de ver el factorial lleva directamente a la recurrencia:


efectivamente, en la definición de factorial nos encontramos que hemos
recurrido al concepto de factorial. La implementación de la función
podría ser la siguiente:
long Factorial(short A)
{
if(A == 0) return 1;
else return a * Factorial(A - 1);
}

Que también podría haberse escrito de la siguiente forma:


long Factorial(short A)
{
return A ? A * Factorial(A - 1) : 1;
}

239
Fundamentos de informática. Programación en Lenguaje C

Un comentario a la función. Cada vez que la función es invocada por sí


misma, se crea de nuevo una variable A, distinta de la variable A creada
en la anterior invocación. Para cada llamada creamos un juego de
variables cuyo ámbito es el de esta llamada, y su vida el tiempo que se
tarde en ejecutar la última sentencia del bloque de la función.

Supongamos que queremos conocer el valor del factorial de 3.


Invocamos a la función Factorial con ese valor como argumento.

printf(“El factorial de %hd es %ld.\n”,3, Factorial(3));

Primera llamada: se crea la variable < A, short , R1 , 3 > . Como A es


distinto de cero, no se devuelve el entero 1, sino el producto de A por el
Factorial de (A – 1). Entonces, antes de terminar la ejecución de la
función Factorial y eliminar la variable A localizada en R1 necesitamos
recibir el valor de Factorial de (A – 1).

Segunda llamada: se crea la variable < A, short , R2 , 2 > . Con el mismo


nombre que en la llamada anterior, son variables diferentes, ubicadas
en posiciones de memoria diferentes. En este momento, la variable en
ámbito es la ubicada en R2 ; la ubicada en R1 no es accesible: siempre
que en esta segunda ejecución de la función Factorial hagamos
referencia a la variable A, se entiende que nos referimos a la ubicada en
R2 . Como esta variable A no vale 0, entonces la función devuelve el
valor del producto de A (la de R2 ) por Factorial(A – 1). Y de nuevo,
antes de terminar la ejecución de la función Factorial y eliminar la
variable A localizada en R2 necesitamos recibir el valor de Factorial(A –
1).

Tercera llamada: se crea la variable < A, short , R3 ,1 > . Con el mismo


nombre que en las dos llamadas anteriores, son variables diferentes,
ubicadas en posiciones de memoria diferentes. En este momento, la
variable en ámbito es la ubicada en R3 ; las ubicadas en R1 y R2 no son
accesibles: siempre que en esta tercera ejecución de la función Factorial
hagamos referencia a la variable A, se entiende que nos referimos a la

240
Capítulo 9. Funciones.

ubicada en R3 . Como esta variable A no vale 0, entonces la función


devuelve el valor del producto de A (la de R3 ) por Factorial(A – 1). Y de
nuevo, antes de terminar la ejecución de la función Factorial y eliminar
la variable A localizada en R3 necesitamos recibir el valor de Factorial(A
– 1).

Cuarta llamada: se crea la variable < A, short , R4 , 0 > . Con el mismo


nombre que en las tres llamadas anteriores, son variables diferentes,
ubicadas en posiciones de memoria diferentes. En este momento, la
variable en ámbito es la ubicada en R4 ; las ubicadas en R1 , R2 y R3 no
son accesibles: siempre que en esta cuarta ejecución de la función
Factorial hagamos referencia a la variable A, se entiende que nos
referimos a la ubicada en R4 . El valor de esta variable es 0 por lo que la
función devuelve el valor 1 y termina su ejecución. La variable A ubicada
en R4 termina su existencia y el control del programa vuelve a quien
llamó a la función.

Quien llamó a la función fue la propia función Factorial, en su tercera


llamada. Estaba pendiente, para cerrarse y devolver un valor, a recibir
un valor de la función Factorial. Y ha recibido el valor 1, que multiplica al
valor de A que también es 1, y devuelve a quien la llamó. La variable A
ubicada en R3 termina su existencia y el control del programa vuelve a
quien llamó a la función.

Y quien llamó a la función fue la propia función Factorial, en su segunda


llamada. Estaba pendiente, para cerrarse y devolver un valor, a recibir
un valor de la función Factorial. Y ha recibido el valor 1, que multiplica al
valor de A que es 2, y devuelve a quien la llamó. La variable A ubicada
en R2 termina su existencia y el control del programa vuelve a quien
llamó a la función.

Y quien llamó a la función fue la propia función Factorial, en su primera


llamada. Estaba pendiente, para cerrarse y devolver un valor, a recibir
un valor de la función Factorial. Y ha recibido el valor 2, que multiplica al
valor de A que es 3, y devuelve a quien la llamó. La variable A ubicada

241
Fundamentos de informática. Programación en Lenguaje C

en R1 termina su existencia y el control del programa vuelve a quien


llamó a la función.

Y quien llamó a la función Factorial fue la función principal, que vuelve a


recuperar el control de ejecución del programa y que recibe el valor
devuelto por la función que se lo pasa como parámetro a la función
printf para que muestre ese valor por pantalla:

El factorial de 3 es 6.

Cuatro ejecuciones, cuatro ámbitos, cuatro variables distintas, cuatro


vidas distintas.

Veamos ahora el segundo ejemplo: el de la función euclides. En esta


función la recurrencia es aún más clara, puesta la propia definición del
algoritmo de Euclides es autodefinida. El código de la función podría
quedar así:
long euclides(long a, long b)
{
return b ? euclides(b, a % b) : a;
}

No vamos a comentar su ejecución con tanta largueza como en la


función Factorial. Pero así escrita, la función euclides se comporta tal y
como quedó definido el algoritmo en el capítulo 8. Allí decíamos:
“Euclides, matemático del siglo V a. de C. presentó un algoritmo muy
fácil de implementar, y de muy bajo coste computacional. El algoritmo
de Euclides dice que el máximo común divisor de dos enteros a1 y b1
(diremos mcd(a1 , b1 ) ), donde b1 ≠ 0 es igual a mcd(a2 , b2 ) donde
a2 = b1 y donde b2 = a1 %b1 , entendiendo por a1 %b1 el resto de la
división de a1 con b1 . Y el proceso puede seguirse hasta llegar a unos
valores de ai y de bi que verifiquen que ai ≠ 0 , bi ≠ 0 y ai %bi = 0 .
Entonces, el algoritmo de Euclides afirma que, llegado a estos valores el
valor buscado es mcd(a1 , b1 ) = bi .” Y eso es precisamente lo que se ha
implementado en esta función.

242
Capítulo 9. Funciones.

Llamadas por valor y llamadas por referencia


Estos dos nuevos conceptos son tradicionales al hablar de funciones. Y
muy importantes. Hacen referencia al modo en que la función recibe los
parámetros.

Hasta ahora, en todos los ejemplos previos presentados, hemos


trabajado haciendo llamadas “por valor”. Decimos que una función es
llamada por valor cuando se copia el valor del argumento en el
parámetro formal de la función. Una variable está en la función que
llama; y otra variable, distinta, es la que recibe el valor en la función
llamada. La función llamada no puede alterar el valor del argumento
original de la función que llama. Únicamente puede cambiar el valor de
su variable local que ha recibido por asignación el valor de esa variable
en el momento en que se realizó la llamada a la función. Así, en la
función llamada, cada argumento es efectivamente una variable local
inicializada con el valor con que se llamó a la función.

Pero supongamos que necesitamos en nuestro programa realizar con


mucha frecuencia la tarea de intercambiar el valor de dos variables. Ya
sabemos cómo se hace, y lo hemos visto resuelto tanto a través de una
variable auxiliar como gracias al operador or exclusivo. Sería muy
conveniente disponer de una función a la que se le pudieran pasar, una
y otra vez, el par de variables de las que deseamos intercambiar sus
valores. Pero ¿cómo lograr hacer ese intercambio a través de una
función si todo lo que se realiza en la función llamada muere cuando
termina su ejecución? ¿Cómo lograr que en la función que invoca ocurra
realmente el intercambio de valores entre esas dos variables?

La respuesta no es trivial: cuando invocamos a la función (que


llamaremos en nuestro ejemplo intercambio), las variables que
deseamos intercambiar dejan de estar en su ámbito y no llegamos a
ellas. Toda operación en memoria que realice la función intercambio
morirá con su última sentencia: su único rastro será, si acaso, la

243
Fundamentos de informática. Programación en Lenguaje C

obtención de un resultado, el que logra sobrevivir de la función gracias a


la sentencia return.

Y aquí llegamos a la necesidad de establecer otro tipo de llamadas a


funciones. las llamadas “por referencia”. En este tipo de llamada, lo
que se transfiere a la función no es el valor del argumento, sino la
dirección de memoria de la variable argumento. Se copia la dirección del
argumento en el parámetro formal, y no su valor.

Evidentemente, en ese caso, el parámetro formal deberá ser de tipo


puntero. En ese momento, la variable argumento quedará fuera de
ámbito, pero a través del puntero correspondiente en los parámetros
formales podrá llegar a ella, y modificar su valor.

La función intercambio podría tener el siguiente prototipo:

void intercambio(long*,long*);

Y su definición podría ser la siguiente:


void intercambio(long*a,long*b)
{
*a ^= *b;
*b ^= *a;
*a ^= *b;
}

O también
void intercambio(long*a,long*b)
{
short aux;
aux = *b;
*b = *a;
*a = aux;
}

Supongamos que la función que llama a la función intercambio lo hace


de la siguiente forma:

intercambio(&x,&y);

Donde lo que le pasa son las direcciones (no los valores) de las dos
variables de las que se desea intercambiar sus valores.

244
Capítulo 9. Funciones.

En la función llamante tenemos:

< x, short , Rx , Vx > y < y , short , Ry , Vy >

En la función intercambio tenemos:

< a, short*, Ra , Rx > y < b, short*, Rb , Ry >

Es decir, dos variables puntero cuyos valores que se le van asignar


serán las posiciones de memoria de cada una de las dos variables
usadas como argumento, y que son con las que se desea realizar el
intercambio de valores.

La función trabaja sobre los contenidos de las posiciones de memoria


apuntadas por los dos punteros. Y cuando termina la ejecución de la
función, efectivamente, mueren las dos variables puntero a y b creadas.
Pero ya han dejado hecha la faena en las direcciones que recibieron al
ser creadas: en Rx ahora queda codificado el valor Vy ; y en Ry queda
codificado el valor Vx . Y en cuanto termina la ejecución de intercambio
regresamos al ámbito de esas dos variables x e y: y nos las
encontramos con los valores intercambiados.

Muchos son los ejemplos de funciones que, al ser invocadas, reciben los
parámetros por referencia. La función scanf recibe el parámetro de la
variable sobre la que el usuario deberá indicar su valor con una llamada
por referencia. También lo hemos visto en la función gets, que recibe
como parámetro la dirección de la cadena de caracteres donde se
almacenará la cadena que introduzca el usuario.

Por otro lado, siempre que deseemos que una función nos devuelva más
de un valor también será necesario utilizar llamadas por referencia: uno
de los valores deseamos podremos recibirlo gracias a la sentencia
return de la función llamada; los demás podrán quedar en los
argumentos pasados como referencia: entregamos a la función sus
direcciones, y ella, al terminar, deja en esas posiciones de memoria los
resultados deseados.

245
Fundamentos de informática. Programación en Lenguaje C

Vectores y matrices como argumentos


Y si podemos pasar la dirección de una variable, entonces también
podemos pasar la dirección de un array, o de una matriz, o de una
cadena de caracteres.

Así, cuando queremos pasar como argumento un vector, no es necesario


hacer copia de todo él en la lista de parámetros: basta pasar como
parámetro la dirección del primer elemento del vector. La función podrá
acceder a todos los elementos del vector mediante operatoria de
punteros o mediante índices.

Habitualmente, al pasar un array o matriz, será necesario pasar, como


otros parámetros, la dimensión o dimensiones de ese array o matriz.

La llamada de la función usará el nombre del vector como argumento,


ya que como dijimos al presentar los arrays y las cadenas, el nombre de
un array o cadena, en C, indica su dirección: decir nombre_vector es lo
mismo que decir &nombre_vector[0].

Evidentemente, y como siempre, el tipo de dato puntero del parámetro


formal debe ser compatible con el tipo de dato del vector argumento.
Existen tres formas de declarar un parámetro formal que va a recibir un
puntero a un vector:

tipo nombre(tipo vector[dimensión]); es decir, declarando el


parámetro como un vector dimensionado.

tipo nombre(tipo vector[]); es decir, declarando el parámetro como


un vector sin tamaño determinado.

tipo nombre(tipo*); es decir, declarando el parámetro como un


puntero.

Veamos un ejemplo. Hagamos una aplicación que reciba un array de


variables tipo float y nos indique cuál es el menor de sus valores y cuál

246
Capítulo 9. Funciones.

el mayor. Entre los parámetros de la función será necesario indicar


también la dimensión del vector.

Lo primero que hemos de pensar es cómo pensamos devolver, a la


función que llame a nuestra función, los dos valores solicitados. Repito:
DOS valores solicitados. No podremos hacerlo mediante un return,
porque así sólo podríamos facilitar uno de los dos. Por eso, entre los
parámetros de la función también necesitamos dos que sean la dirección
donde deberemos dejar recogido el mayor de los valores y la dirección
donde deberá ir recogido el menor de ellos.

Entonces la función podrá tener el siguiente prototipo:

void extremos(float*v, unsinged short d, float*M, float*m);

El primer parámetro es la dirección del array donde se recogen todos los


valores. En segundo parámetro la dimensión del array. El tercero y el
cuarto las direcciones donde se consignarán los valores mayor (variable
M) y menor (variable m) del array.

El código de la función podría ser el siguiente:


void extremos(float*v, unsigned short d, float*M, float*m)
{
short int i;
*M = *v;
*m = *v;
for(i = 0 ; i < d ; i++)
{
if(*M < *(v + i)) *M = *(v + i);
if(*m > *(v + i)) *m = *(v + i);
}
}

Inicialmente hemos puesto como menor y como mayor el primero de los


elementos del vector. Y luego lo hemos recorrido, y siempre que hemos
encontrado un valor mayor que el que teníamos consignado como
mayor, hemos cambiado y hemos guardado ese nuevo valor como el
mayor; y lo mismo hemos hecho con el menor. Y al terminar de recorrer
el vector, ya han quedado esos dos valores guardados en las direcciones
de memoria que hemos recibido como parámetros.

247
Fundamentos de informática. Programación en Lenguaje C

Para llamar a esta función bastará la siguiente sentencia:

extremos(vector, dimension, &mayor, &menor);

La recepción de un vector como parámetro formal no necesariamente


debe hacerse desde el primer elemento del vector. Supongamos que al
implementar la función extremos exigimos, como especificación técnica
de esa función, que la matriz tenga una dimensión impar. Y diremos que
reciba como parámetros los mismos que antes. Pero ahora la dirección
de memoria del vector será la del elemento que esté a la mitad del
vector. Si la dimensión es n , el usuario de la función deberá pasar como
argumento la dirección del elemento de índice (n − 1) / 2 . Y el argumento
segundo deberá ser, en lugar de la dimensión del vector, el valor
indicado (n − 1) / 2 .

El prototipo de la función es exactamente el mismo que antes:

void extremos2(float*v, unsinged short d, float*M, float*m);

Y su definición podría ser la siguiente:


void extremos2(float*v, unsigned short d, float*M, float*m)
{
short int i;
*M = *v;
*m = *v;
for(i = 0 ; i < d ; i++)
{
if(*M < *(v + i)) *M = *(v + i);
if(*M < *(v - i)) *M = *(v - i);
if(*m > *(v + i)) *m = *(v + i);
if(*M < *(v - i)) *M = *(v - i);
}
}

Donde así logramos la misma operación de búsqueda y hemos reducido


a la mitad los incrementos de la variable contador i. No entramos ahora
en analizar la oportunidad de esta nueva versión de la función.
Queremos señalar simplemente que el código puede hacer lo que a
nosotros nos convenga más. Lo importante en este caso es dejar bien
especificadas las condiciones para el correcto uso de la función. Y vigilar

248
Capítulo 9. Funciones.

cuáles son los límites del vector y no permitir que se acceda a posiciones
de memoria que están fuera de la dimensión del vector.

Funciones de escape
Existen ocasiones en que lo mejor que se puede hacer es abortar a
ejecución de una aplicación antes de llegar a consecuencias más
desastrosas si se continuara la ejecución del programa. A veces más
vale abortar misión intentando salvar la mayor parte de los muebles,
que dejar que una situación irresoluble arruine la línea de ejecución y
entonces se produzca la interrupción de la ejecución de una forma no
controlada por nosotros.

Para realizar esas operaciones de salida inmediata disponemos de dos


funciones, definidas en la biblioteca stdlib.h: la función exit y la función
abort.

La función exit nos permite abandonar la ejecución de un programa


antes de su finalización, volviendo el control al sistema operativo. Antes
de regresar ese control, realiza algunas tareas importantes: por
ejemplo, si el programa tiene abiertos algunos archivos, la función los
cierra antes de abandonar la ejecución de la aplicación, y así esos
archivos no se corrompen. Esta función se utiliza cuando no se cumple
una condición, imprescindible para la correcta ejecución del programa.

El prototipo de la función es

void exit(int status);

El parámetro status indica el modo en que se debe realizar la operación


de finalización inmediata del programa. El valor habitual es el cero, e
indica que se debe realizar una salida inmediata llamada normal.

Ahora mismo no vamos a poner ejemplos de uso de esta función. Pero


más adelante, en el próximo tema, se verán ocasiones claras en que su
uso es muy necesario.

249
Fundamentos de informática. Programación en Lenguaje C

La función abort es semejante. Su prototipo es:

void abort(void)

Y nos permite abandonar de forma anormal la ejecución del programa,


antes de su finalización. Escribe el mensaje “Abnormal program
termination” y devuelve el control al sistema operativo.

Ejercicios

54. Escribir un programa que solicite al usuario dos enteros y


calcule, mediante una función, el máximo común divisor.
Definir otra función que calcule el mínimo común múltiplo,
teniendo en cuenta que siempre se verifica que
a × b = mcd(a, b) × mcm(a, b) .

#include <stdio.h>

// Declaración de las funciones ...


short mcd(short, short);
long mcm(short, short);

// Función principal...
void main(void)
{
short a, b;

printf("Introduzca el valor de a ... ");


scanf("%hd",&a);

printf("Introduzca el valor de b ... ");


scanf("%hd",&b);

printf("El máximo común divisor de %hd y %hd ", a, b);


printf("es %hd", mcd(a,b));
printf("\ny el mínimo común múltiplo %ld.", mcm(a,b));

flushall();
getchar();
}

250
Capítulo 9. Funciones.

/* ------------------------------------------------------- */
/* Definición de las funciones */
/* ------------------------------------------------------- */

/* Función cálculo del máximo común divisor. ------------- */


short mcd(short a, short b)
{
short m;
while(b)
{
m = a % b;
a = b;
b = m;
}
return a;
}

/* Función cálculo del mínimo común múltiplo. ------------ */


long mcm(short a, short b)
{
return a * (long)b / mcd(a, b);
}

55. Haga un programa que calcule el término n (a determinar en la


ejecución del programa) de la serie de Fibonacci. El programa
deberá utilizar una función, llamada fibonacci, cuyo prototipo
sea

short fibonacci(short);

Que recibe como parámetro el valor de n, y devuelve el término


n-ésimo de la Serie.

#include <stdio.h>
#include <conio.h>

unsigned long fibonacci(short);

void main(void)
{
short N;
printf("Indique el término de la serie: ");

251
Fundamentos de informática. Programación en Lenguaje C

scanf("%hd", &N);

printf("\nEl término %hd es %lu.", N, fibonacci(N));


}

/* ------------------------------------------------------- */
/* Definición de las funciones */
/* ------------------------------------------------------- */

/* Función Fibonacci. ------------------------------------ */


unsigned long fibonacci(short x)
{
unsigned long fib1 = 1 , fib2 = 1, Fib = 1;

while(x > 2)
{
Fib = fib1 + fib2;
fib1 = fib2;
fib2 = Fib;
x--;
}
return Fib;
}

56. Escriba un programa que solicite al usuario un entero y


devuelva el factorial de ese entero. Utilice una función para el
cálculo del factorial.

#include <stdio.h>

// Declaración de la funsión Factorial ...


unsigned long factorial(short);
// Función principal ...
void main(void)
{
short N;
printf("Indique un entero menor que 13: ");
scanf("%hd", &N);

printf("\nEl factorial de %hd”, N);


printf(“ es %lu.", factorial(N));
}

/* ------------------------------------------------------- */

252
Capítulo 9. Funciones.

/* Definición de las funciones */


/* ------------------------------------------------------- */

/* Función Factorial. ------------------------------------ */


unsigned long factorial(short x)
{
unsigned long F = 1;
while(x) F *= x--;
return F;
}

57. Escriba una función que reciba como parámetros un vector de


enteros y un entero que recoja la dimensión del vector, y
devuelva ese vector, con los valores enteros ordenados de
menor a mayor.

Un posible programa que utilice esta función que presentamos podría


ser el siguiente:
#include <stdio.h>
#include <stdlib.h>

#define TAM 100

// Declaración de las funciones ...


void ordenar(long *, short);
void mostrar(long *, short);

// Función principal ...


void main(void)
{
long vector[TAM];

randomize();
for(int i = 0 ; i < TAM ; i++)
vector[i] = random(1000);

mostrar(vector, TAM); // Antes de ordenar.


ordenar(vector, TAM); // Se ordena el vector.
mostrar(vector, TAM); // Despúes de ordenar.
}

/* ------------------------------------------------------- */
/* Definición de las funciones */
/* ------------------------------------------------------- */

253
Fundamentos de informática. Programación en Lenguaje C

/* Función de ordenación. -------------------------------- */


void ordenar(long *v , short d)
{
for(short i = 0 ; i < d ; i++)
for(short j = i + 1 ; j < d ; j++)
if(*(v + i) > *(v + j))
{
*(v + i) ^= *(v + j);
*(v + j) ^= *(v + i);
*(v + i) ^= *(v + j);
}
}

/* Función que muestra el vector que recibe como parámetro.*/


void mostrar(long *v , short d)
{
printf("\n\n");
for(short i = 0 ; i < d ; i++)
printf("%5ld", *(v + i));
}

Hewmos definido dos funciones: una muestra las valores de un vector


que recibe como primer parámetro, cuya dimensión queda indicada por
el segundo parámetro.

Las dos funciones reciben como parámetros el valor de la dimensión,


que es un valor tomado de una directiva define, prefectamente accesible
en cualquier punto del programa. Y es que aunque el valor de la macro
TAM es accesible desde ambas funciones, éstas, como es exigido en un
correcto diseño de funciones, no dependen para nada del entorno en el
que están definidas. Tal y como están implementadas en nuestro
ejercicio pueden ser exportadas a cualquier sitio, pues para nada
depende su correcta ejecución de ningún valor o parámetro, o macro,
definido en el entorno en el que se han definido aquí y ahora ambas
funciones.

En todo momento se ha utilizado en las fucniones la aritmética de


punteros y el operador indirección. Evidentemente se podría hacer lo
mismo con los índices del vector. De hecho la expresión *(v + i) es
siempre intercambiable por la expresión v[i].

254
Capítulo 9. Funciones.

58. Escriba una aplicación que reciba un entero y busque todos sus
divisores, dejándolos en un vector.

La aplicación debe tener una función que se encargará de


buscar los divisores. Esta función recibirá como parámetros.el
entero sobre el que hay que buscar sus divisores, el vector
donde debe dejar los enteros, y la dimensión del vector.

La función devuelve un valor positivo igual al número de


divisores hallados si el proceso termina correctamente. Deberá
devolver un valor negativo si falla en el proceso de búsqueda
de los divisores.

#include <stdio.h>

#define TAM 100

// Declaración de las funciones ...


short divisores(long, long*, short);
void mostrar(long *, short);

// Función principal ...


void main(void)
{
long N, vector[TAM];
short div;

printf("Introduzca un entero ... ");


scanf("%ld",&N);

if((div = divisores(N,vector, TAM)) > 0)


mostrar(vector, div);
else
printf("No se ha podido realizar la operación");
}

/* ------------------------------------------------------- */
/* Definición de las funciones */
/* ------------------------------------------------------- */

255
Fundamentos de informática. Programación en Lenguaje C

/* Función de búsqueda de divisores. --------------------- */


short divisores(long N, long *v , short d)
{
short cont = 1;
v[0] = 1;
for(int div = 2 ; div <= N / 2 ; div++)
if(N % div == 0)
{
v[cont] = div;
cont++;
// Si no caben más divisores, se aborta la operación.
if(cont >= d) return -1;
}
v[cont] = N;
return cont + 1;
}

/* Función que muestra el vector que recibe como parámetro.*/


void mostrar(long *v , short d)
{
printf("\n\n");
for(short i = 0 ; i < d ; i++)
printf("%5ld", *(v + i));
}

59. Escriba una función, y un programa que le use, que calcule el


máximo común divisor de un conjunto de enteros que recibe en
un vector como parámetro Además, la función recibe otro
parámetro que es el número de enteros recogidos en ese
vector.

#include <stdio.h>
#define TAM 100

// Declaración de funciones ...


long mcd(long, long);
long MCD(long*,short);

// Función principal ...


void main(void)
{
long numeros[100];
long m;
short i;

256
Capítulo 9. Funciones.

// Introducción de valores ...

printf("Introduzca valores.");
printf("Al terminar introduzca en valor cero.");
for(i = 0 ; i < TAM ; i++)
{
printf("\n\nnumeros[%3hu] -> ",i);
scanf("%lu",numeros + i);
if(*(numeros + i) == 0) break;
}

// Cálculo del máximo común divisor ...


m = MCD(numeros,i);
printf("El maximo comun divisor es ... %lu",m);
}

/* ------------------------------------------------------- */
/* Definición de las funciones */
/* ------------------------------------------------------- */

// Función cálculo mcd de dos enteros ...


long mcd(long a, long b) { return b ? mcd(b,a % b) : a; }

// Función cálculo mcd de varios enteros ...


long MCD(long*n,short d)
{
short i;
long m;

// Si algún entero es 0, el mcd lo ponemos a cero.


for(i = 0 ; i < d ; i++)
if(!*(n + i)) return 0;
// Si solo hay un entero, el mcd es ese entero.
if(d == 1) return *(n + 0);

i = 2;
m = mcd(*(n + 0),*(n + 1));
while(i < d) m = mcd(m,*(n + i++));

return m;
}

60. Escriba una función que reciba un entero y diga si es o no es


perfecto (devuelve 1 si lo es; 0 si no lo es). Utilice esa función
para mostrar los números perfectos entre dos enteros

257
Fundamentos de informática. Programación en Lenguaje C

introducidos por el usuario.

#include <stdio.h>
#include <conio.h>

// Declaración de la función perfecto ...


short perfecto(long);

// Función principal ...


void main(void)
{
long a, b;

printf("Limite inferior ... ");


scanf("%ld",&a);

printf("Limite superior ... ");


scanf("%ld",&b);

for(long i = a ; i <= b; i++)


if(perfecto(i) == 1) printf("%6ld", i);
}

/* ------------------------------------------------------- */
/* Definición de las funciones */
/* ------------------------------------------------------- */

// Función perfecto ...


short perfecto(long x)
{
long suma = 1;
for(long div = 2 ; div <= x / 2 ; div++)
if(x % div == 0) suma += div;

return suma == x ? 1 : 0;
}

61. Torres de Hanoi. Escriba un programa que solicite al usuario


con cuántos aros de la torre de Hanoi desea jugar, y que el
programa muestre por pantalla todos los movimientos
necesarios para trasladoar todos los aros del primer al tercer
soporte o varilla.

258
Capítulo 9. Funciones.

(Consultar “Fundamentos de Informática. Codificación y algoritmia”,


Capítulo 6 sobre Recursividad.)

#include <stdio.h>

// Declaración de la función Hanoi ...


void Hanoi(short, short, short, short);

// Función principal ...


void main(void)
{
short discos;

printf("Indique con cuántos discos desea jugar: ");


scanf("%hd",&discos);

Hanoi(1, discos, 1, 3);


}

/* ------------------------------------------------------- */
/* Función Hanoi. */
/* ------------------------------------------------------- */

void Hanoi(short D1, short D2, short i, short j)


{
if(D1 == D2)
printf("Disco %hu de %hu a %hu\n",D1, i, j);
else
{
Hanoi(D1 , D2 - 1 , i , 6 - i - j);
Hanoi(D2 , D2 , i , j);
Hanoi(D1 , D2 - 1 , 6 - i - j , j);
}
}

62. Serie de Fibonacci. Fibonacci fue un matemático italiano del


siglo XIII que descubrió la serie que lleva su nombre. Cada
número de esa serie es el resultado de la suma de los dos
anteriores:

1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, ...

Haga un programa que calcule la serie de Fibonacci y guarde

259
Fundamentos de informática. Programación en Lenguaje C

los 40 primeros elementos de la serie en un array de tipo


unsigned long. (Tenga en cuenta que el elemento 46 de la serie
de Fibonacci es el mayor elemento de la serie codificable en un
entero sin signo de 32 bits: si calcula valores más allá de esta
posición, debería definir un tipo de dato nuevo que albergara
enteros más largos.)

Esta serie goza de una serie de propiedades curiosas. Por


ejemplo:

D La suma de los n primeros términos verifica que:


n

∑f
i =1
i = fn+2 − 1

D La suma de los n primeros términos pares verifica que:


n

∑f
i =1
2⋅i = f2⋅n+1 − 1

D La suma de los n primeros términos impares verifica que:


n

∑f
i =1
2⋅i −1 = f2⋅n

D La suma de los cuadrados de los n primeros términos


verifica que:
n

∑f
2
i = fn ⋅ fn+1
i =1

D Si n es divisible por m, entonces fn es divisible por fm .

D Cualesquiera dos elementos consecutivos de la serie de


Fibonacci son primos entre sí.

D El cociente de dos números consecutivos de la serie se


aproxima al número áureo: fn+1 fn → α cuando n → ∞ .

Continúe el programa anterior de forma que el usuario pueda


solicitar mediante un menú de opciones, la comprobación de

260
Capítulo 9. Funciones.

cada una de estas propiedades...

Antes de desanimarse ante la longitud de este ejercicio propuesto y


dejarlo estar, vale la pena que al menos considere que es un ejemplo
muy sencillo, con funciones simples, todas ellas muy parecidas. Y que si
bien es cierto que es mucho código para copiar en un ordenador y ver
cómo funciona, sí se puede detener en una o dos de las 9 funciones
definidas, y verlas en funcionamiento.

Un posible código que resuelve el problema planteado podría ser el


siguiente:

/* ======================================================= */
/* PROGRAMA DE LA SERIE DE FIBONACCI. */
/* ======================================================= */

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

#define RANGO 40

// Declaración de las funciones...


char menu(void);
void fib01(unsigned long*);
void fib02(unsigned long*);
void fib03(unsigned long*);
void fib04(unsigned long*);
void fib05(unsigned long*);
void fib06(unsigned long*);
long mcd(long, long);
void fib07(unsigned long*);
void fib08(unsigned long*);

// Función principal...
void main(void)
{
unsigned long fib[RANGO + 1];
// No utilizaremos el elemento 0.
unsigned short i;
char opcion;

fib[1] = 1;

261
Fundamentos de informática. Programación en Lenguaje C

fib[2] = 1;
// Serie de fibonacci...
for(i = 3 ; i <= RANGO ; i++)
fib[i] = fib[i - 1] + fib[i - 2];

do
{
// Menú de opciones ...
opcion = menu();
switch(opcion)
{
case '1': fib01(fib); break;
case '2': fib02(fib); break;
case '3': fib03(fib); break;
case '4': fib04(fib); break;
case '5': fib05(fib); break;
case '6': fib06(fib); break;
case '7': fib07(fib); break;
case '8': fib08(fib); break;
case '0': printf("\nFin del programa");
printf("\nPulse tecla para terminar");
flushall();
getchar();
break;
default: printf("opción no definida ... ");
}
}while(opcion != '0');
}

/* ------------------------------------------------------- */
/* Definición de las funciones */
/* ------------------------------------------------------- */

/* Función que muestra el menú de opciones */


char menu(void)
{
char opcion;

clrscr();

printf("\n\n\tPROPIEDADES DE LA SERIE DE FIBONACCI");


printf("\n\n\t0. Salir de programa.");
printf("\n\t1. La suma de los n primeros terminos");
printf("\n\t es igual a f[n + 2] - 1.");

printf("\n\t2. La suma de los n primeros


terminos pares (de 2 a 2*n)");
printf("\n\t es igual a f[2 * n + 1] - 1.");

printf("\n\t3. La suma de los n primeros

262
Capítulo 9. Funciones.

terminos impares (de 1 a 2*n-1)");


printf("\n\t es igual a f[n + 2].");

printf("\n\t4. La suma de los cuadrados


de los n primeros terminos");
printf("\n\t es igual a f[n] * f[n + 1].");

printf("\n\t5. Si n es divisible por m,


entonces f[n]");
printf("\n\t es divisible por f[m].");

printf("\n\t6. Cualquier par consecutivo


de fibonacci");
printf("\n\t son primos entre si.");

printf("\n\t7. El cociente de dos elementos


consecutivos de la serie");
printf("\n\t se aproxima al numero aureo.");

printf("\n\t8. Mostrar los 40 primeros


elementos de la serie de Fibonacci.");

printf("\n\n\n\tElija una opcion ... ");

do
opcion = getchar();
while(opcion < '0' || opcion > '8');
return opcion;
}

// Función fib01
void fib01(unsigned long*f)
{
unsigned short n, i;
unsigned long S = 0;

clrscr();
printf("\n\n\n\tOPCION SELECCIONADA 1.");
printf("\n\n\tLa suma de los n primeros terminos");
printf("\n\tes igual a f[n + 2] - 1.");

printf("\n\nIndique el indice n que quiere


verificar (entre 1 y 38) ... ");
scanf("%hu",&n);
if(n > RANGO - 2)
printf("No se disponen de suficientes
elementos.");
else if(n == 0) printf("No valido");
else
{
for(i = 1 ; i <= n ; i++) S += *(f + i);

263
Fundamentos de informática. Programación en Lenguaje C

printf("\n\nSUMA f[ 1]... f[%2hu] = %lu\n",n,S);


printf("f[%hu]-1 = %lu\n",n + 2,*(f + n + 2)-1);
}

printf("\n\nPulse intro para volver a menu principal");


getchar();
}

// Función fib02
void fib02(unsigned long*f)
{
unsigned short n, i;
unsigned long S = 0;

clrscr();
printf("\n\n\n\tOPCION SELECCIONADA 2.");
printf("\n\tLa suma de los n primeros
terminos pares (2, 4, ..., 2 * n)");
printf("\n\tpares es igual a f[2 * n + 1] - 1.");

printf("\n\nIndique el indice n que quiere


verificar (entre 1 y 19) ... ");
scanf("%hu",&n);

// n no puede ser tal que 2 * n + 1 sea mayor que RANGO...


if(n > (RANGO - 1) / 2)
printf("No se disponen de suficientes
elementos.");
else if(n == 0) printf("No valido");
else
{
for(i = 2 ; i <= 2 * n ; i += 2) S += *(f + i);
// Mostramos resultados...
printf("\n\nSUMA f[ 2]... f[%2hu] =
%lu\n",2 * n,S);
printf("f[%hu] - 1 = %lu\n",
2 * n + 1,*(f + 2 * n + 1) - 1);
}

printf("\n\nPulse intro para volver a menu principal");


getchar();
}

// Función fib03
void fib03(unsigned long*f)
{
unsigned short n, i;
unsigned long S = 0;

clrscr();
printf("\n\n\n\tOPCION SELECCIONADA 2.");

264
Capítulo 9. Funciones.

printf("\n\tLa suma de los n primeros terminos


impares (de 1 a 2*n-1)");
printf("\n\tes igual a f[n + 2].");

printf("\n\nIndique el indice n que quiere


verificar (entre 1 y 20) ... ");
scanf("%hu",&n);

// n no puede ser tal que 2 * n - 1 sea mayor que RANGO...


if(n > (RANGO + 1) / 2)
printf("No se disponen de suficientes
elementos.");
else if(n == 0) printf("No valido");
else
{
for(i = 1 ; i <= 2 * n - 1 ; i += 2)
S += *(f + i);
// Mostramos resultados...
printf("\n\nSUMA f[ 2]... f[%2hu] = %lu\n",
2 * n,S);
printf("f[%hu] = %lu\n",2 * n,*(f + 2 * n));
}

printf("\n\nPulse intro para volver a menu principal");


getchar();
}

// Función fib04
void fib04(unsigned long*f)
{
unsigned short n, i;
unsigned long S = 0;

clrscr();
printf("\n\n\n\tOPCION SELECCIONADA 3.");
printf("\n\tLa suma de los cuadrados de los
n primeros terminos");
printf("\n\tes igual a f[n] * f[n + 1].");

printf("\n\nIndique el indice n que quiere verificar


(entre 1 y 39) ... ");
scanf("%hu",&n);
if(n > RANGO - 1)
printf("No se disponen de suficientes
elementos.");
else if(n == 0) printf("No valido");
else
{
for(i = 1 ; i <= n ; i++)
S += *(f + i) * *(f + i);

265
Fundamentos de informática. Programación en Lenguaje C

printf("\n\nSUMA f[ 1]^2... f[%2hu]^2 = %lu\n",


n,S);
printf("f[%hu] * f[%hu] = %lu\n",
n,n + 1,*(f + n) * *(f + n + 1));
}

printf("\n\nPulse intro para volver a menu principal");


getchar();
}

// Función fib05
void fib05(unsigned long*f)
{
unsigned short n, m;

clrscr();
printf("\n\n\n\tOPCION SELECCIONADA 5.");
printf("\n\tSi n es divisible por m, entonces f[n]");
printf("\n\tes divisible por f[m].\n\n");

for(n = 3 ; n < RANGO / 2 ; n++)


// Todos los múltiplos de i ...
{
/* Comenzamos con n = 3, porque f[2] = 1, y todos serán
múltiplos de 1. */
printf("\n\n** n = %2hu\tf[%2hu] = %10lu ** \n",
n,n,*(f + n));
for(m = 2 * n ; m < RANGO ; m += n)
{
printf(" m = %2hu\tf[%2hu] = %10lu\t",
m, m, *(f + m));
printf("f[%2hu] %% f[%2hu] = %lu",
m,n,*(f + m) % *(f + n));
if(*(f + m) % *(f + n) == 0)
printf("\t[DIVISIBLE]\n");
}
printf("\n\n\tPulse una tecla para ver siguiente
serie de multiplos ... ");
getchar();
}
printf("\n\nPulse intro para volver a menu principal");
getchar();
}

// Función fib06
void fib06(unsigned long*f)
{
unsigned short n;

clrscr();
printf("\n\n\n\tOPCION SELECCIONADA 6.");

266
Capítulo 9. Funciones.

printf("\n\tCualquier par consecutivo de fibonacci");


printf("\n\tson primos entre si.\n\n");

for(n = 3 ; n < RANGO ; n++)


{
printf("f[%2hu] = %10lu\t",n,*(f + n));
printf("f[%2hu] = %10lu\t",n + 1, *(f + n + 1));
printf("MCD = %lu\t",mcd(*(f + n),*(f + n + 1)));
if(mcd(*(f + n),*(f + n + 1)) == 1)
printf("[COPRIMOS]\n");
}
printf("\n\nPulse intro para volver a menu principal");
getchar();
}

// Función mcd, definida para uso de fib06


long mcd(long a, long b)
{
return b ? mcd(b,a % b) : a;
}

// Función fib07
void fib07(unsigned long*f)
{
unsigned short n;
const double oro = (1 + sqrt(5)) / 2;
char blancos[] = " ";

clrscr();
printf("\n\n\n\tOPCION SELECCIONADA 7.");
printf("\n\tEl cociente de dos elementos
consecutivos de la serie");
printf("\n\tse aproxima al numero aureo.\n\n");

for(n = 1 ; n < RANGO ; n++)


{
printf("f[%2hu] = %10lu\t",n,*(f + n));
printf("f[%2hu] = %10lu\t",n + 1, *(f + n + 1));
printf("COCIENTE = %20.18lf\n"
(double)*(f + n + 1) / *(f + n));
}
printf("\n%sNumero de oro (1+sqrt(5)) / 2 -> %20.18lf",
blancos,oro);
printf("\n\nPulse intro para volver a menu principal");
getchar();
}

// Función fib08
void fib08(unsigned long*f)
{
unsigned short n;

267
Fundamentos de informática. Programación en Lenguaje C

clrscr();
printf("\n\n\n\tOPCION SELECCIONADA 8.");
printf("\n\tMostrar los 40 primeros elementos
de la serie de Fibonacci.\n\n");

for(n = 1 ; n <= RANGO ; n++)


printf("f[%2hu] = %10lu%s",
n,*(f + n),n % 2 ? "\t" : "\n");

printf("\n\nPulse intro para volver a menu principal");


getchar();
}

63. Haga un programa que solicite al usuario un valor entero entre


1 y 999.999 y escriba por pantalla la cantidad numérica
introducida escrito con letras. Por ejemplo, si el usuario
introduce 56343, el programa deberá escribir por pantalla
“Cincuenta y seis mil trescientos cuarenta y tres”.

El programa planteado es bastante largo y el enunciado desanima,


porque de entrada se vislumbran tantas posibilidades que aburre. Sin
embargo su lógica es sencilla y gracias a las funciones es también fácil
de entender.

Han quedado definidas tres funciones. Una llamada unidades, otra


decenas, y otra centenas. Al obtener el número a leer en formato texto,
lo primero que hace la función principal es determinar si, efectivamente,
ese número introducifo tiene unidades, decenas, centenas, unidades de
millar, decenas de millar y centenas de millar. Según las tenga o no se
invocará a las funciones que expresan las unidades, o las decenas, o las
centenas.

La función de las decenas es algo más complicada porque ha de


contemplar la forma de expresar la numeración entre el once y el
quince, distinto al resto de decenas. Y además ha de tener en cuenta

268
Capítulo 9. Funciones.

que si no hay unidades, entonces las decenas no añaden, al final, la


cópula “y”.
#include <stdio.h>
#include <string.h>

// Declaración de las funciones


char* unidades(unsigned short, unsigned short);
char* decenas(unsigned short, unsigned short);
char* centenas(unsigned short,unsigned short,unsigned short);

/Función principal
void main(void)
{
char leido[200], leidoM[200];
unsigned long int n, naux;
unsigned short C, D, U, c, d, u;

do
{
// Inicializamos las cadenas de caracteres.
leido[0] = '\0';
leidoM[0] = '\0';

printf("\n\n\nIntroduzca entero a ... ");


scanf("%lu",&naux);
// Determinamos unidades, decenas, centenas...
// y unidades, decenas y centenas de mil
n = naux;
u = naux % 10;
naux /= 10;
d = naux % 10;
naux /= 10;
c = naux % 10;
naux /= 10;
U = naux % 10;
naux /= 10;
D = naux % 10;
naux /= 10;
C = naux % 10;

if(U || D || C)
{
// Sin el número es mayor que 999:
strcat(leidoM,centenas(C,D,U));
strcat(leidoM,decenas(U,D));
strcat(leidoM,unidades(U,D));
strcat(leidoM, "mil ");
}
strcat(leido,centenas(c,d,u));
strcat(leido,decenas(u,d));

269
Fundamentos de informática. Programación en Lenguaje C

strcat(leido,unidades(u,d));
strcat(leidoM,leido);

printf("El numero %lu se lee ... ",n);


printf("\n%s",leidoM);
}while(n); // Termina la aplicación cuando el usuario
// quiera leer el número cero.
}

/* ------------------------------------------------------- */
/* Definición de las funciones */
/* ------------------------------------------------------- */

/* Función para las unidades. ---------------------------- */


char* unidades(unsigned short u, unsigned short d)
{
if(d == 1 && (u == 0 || u == 1 || u == 2 ||
u == 3 || u == 4 || u == 5))
return "";

switch(u)
{
case 0: return "";
case 1: return "uno ";
case 2: return "dos ";
case 3: return "tres ";
case 4: return "cuatro ";
case 5: return "cinco ";
case 6: return "seis ";
case 7: return "siete ";
case 8: return "ocho ";
case 9: return "nueve ";
}
}

/* Función para las decenas. ----------------------------- */


char* decenas(unsigned short u, unsigned short d)
{
if(d == 1 && (u == 0 || u == 1 || u == 2 ||
u == 3 || u == 4 || u == 5))
switch(u)
{
case 0: return "diez ";
case 1: return "once ";
case 2: return "doce ";
case 3: return "trece ";
case 4: return "catorce ";
case 5: return "quince ";
}
switch(d)
{

270
Capítulo 9. Funciones.

case 0: return "";


case 1: return "dieci";
case 2: if(u) return "veinti";
else return "veinte";
case 3: if(u) return "treinta y ";
else return "treinta ";
case 4: if(u) return "cuarenta y ";
else return "cuarenta ";
case 5: if(u) return "cincuenta y ";
else return "cincuenta";
case 6: if(u) return "sesenta y ";
else return "sesenta ";
case 7: if(u) return "setenta y ";
else return "setenta ";
case 8: if(u) return "ochenta y ";
else return "ochenta ";
case 9: if(u) return "noventa y ";
else return "noventa ";
}
}

/* Función para las centenas. ---------------------------- */


char* centenas(unsigned short c,unsigned short d,
unsigned short u)
{
switch(c)
{
case 0: return "";
case 1: if(!u && !d) return "cien ";
else return "ciento ";
case 2: return "doscientos ";
case 3: return "tresientos ";
case 4: return "cuatrocientos ";
case 5: return "qinientos ";
case 6: return "seiscientos ";
case 7: return "setecientos ";
case 8: return "ochocientos ";
case 9: return "novecientos ";
}
}

271
Fundamentos de informática. Programación en Lenguaje C

272
PARTE II:
Profundizando
en C.
Fundamentos de informática. Programación en Lenguaje C

274
PARTE II: Profundizando en C.

Para el estudio de esta segunda parte del manual es muy conveniente


estar convencidos de que se domina la primera parte.

Ya hemos visto muchos conceptos sobre el modo de programar en C.


Pero son muchísimos más los que quedan por conocer. Desde luego, no
se pretende en este manual abordarlos todos. Por ejemplo, no se dice
una sola palabra sobre la programación en un entorno gráfico.

En esta segunda parte vamos a hablar, en cuatro capítulos, de la


asignación dinámica de la memoria, y de cómo disponer de arrays y
matrices de dimensión definida en tiempo de ejecución. También
hablaremos de otras muchas posibilidades que se pueden abordar
mediante las funciones, y técnicas de manejar las funciones: punteros a
funciones, funciones como argumentos de otra función, funciones con un
número variable de argumentos, macros, etc. En un tercer capítulo
mostraremos técnicas para crear nuevos tipos de datos distintos de los
tipos de dato primitivos. Y terminamos esta segunda parte y el manual
hablando del acceso a disco.

275
Fundamentos de informática. Programación en Lenguaje C

276
CAPÍTULO 10
ASIGNACIÓN DINÁMICA DE
MEMORIA

Cuando hemos hablado de los vectores, hemos remarcado la idea de


que la dimensión indicada en su declaración debe ser un literal:

long numeros[100];

Con esa sentencia creamos un vector de 100 enteros largos: acabamos


de reservar 400 bytes para codificar valores de este tipo de dato.

En muchas ocasiones nos podría interesar que la declaración de este


vector se hiciera mediante una variable, y poder así reservar el espacio
de memoria que realmente necesitáramos. Por ejemplo, al crear unas
matrices en un programa de operatoria de matrices, vendría bien poder
codificar algo del siguiente estilo:
long f, c;
printf(“Número de filas de su matriz A ... ”);
scanf(“%ld”,&f);
printf(“Número de columnas de su matriz A ... ”);
scanf(“%ld”,&c);
Fundamentos de informática. Programación en Lenguaje C

long A[f][c];

Y así, quedaría creada la matriz de acuerdo con la dimensión deseada.

Pero este código es erróneo.

Esta forma de crear matrices no puede llevarse a cabo bajo ningún


concepto, porque la reserva de memoria de una matriz o de un vector
se realiza en tiempo de compilación: el compilador, antes de ejecución
alguna, debe saber cuánta memoria se debe reservar y para qué
dominio de valores.

Eso es un serio problema. Si deseamos hacer un programa que necesite


manejar información con arrays o matrices, siempre habrá que
dimensionar esos espacios de memoria de manera que consideren el
caso más desfavorable: para la ocasión en que se necesite un mayor
tamaño. Y luego recorrer ese array o esa matriz acotando el uso a la
zona que nos interesa.

Un ejemplo simple: si queremos crear una matriz cuadrada, y su


dimensión puede estar en un valor entre 2 y 100, el programador
deberá hacer lo siguiente:
long matriz[100][100];
short int dimension;
short int i, j;
printf(“Indique la dimensión de la matriz ... ”);
scanf(“%hd”,&dimension);
// Introducción de valores...
for( i = 0 ; i < dimension ; i++)
for(j = 0 ; j < dimension j++)
{
printf(“matriz[%hd][%hd] = ”, i, j);
scanf(“%ld”,&matriz[i][j]);
}

Y así, si el usuario introduce el valor 3, entonces emplearemos 36 bytes


para el manejo de nuestra matriz de 9 enteros largos. Pero la memoria
reservada será de cuarenta mil bytes.

C dispone de un modo para lograr que la reserva de memoria quede


optimizada, haciendo esa reserva en tiempo de ejecución, cuando el

278
Capítulo 10. Asignación dinámica de memoria.

programa se ejecuta y ya sabe con exactitud la memoria que necesita:


mediante la asignación dinámica de memoria. Con ello se logra ajustar
la cantidad de memoria que utiliza el programa al tamaño que
realmente es necesario para cada ejecución concreta

Disponemos de algunas funciones que nos permiten reservar y liberar


esa memoria de una forma dinámica, es decir, en tiempo de ejecución.

Función malloc
Esta función malloc queda definida en la biblioteca stdlib.h. También la
encontramos en la biblioteca alloc.h. Recomendamos hacer uso siempre
de la definición recogida en stdlib.

Su prototipo es el siguiente:

void *malloc(size_t size);

Donde el tipo de dato size_t lo consideraremos ahora nosotros


simplemente como un tipo de dato long.

La función reserva tantos bytes consecutivos como indique el valor de la


variable size y devuelve la dirección del primero de esos bytes. Esa
dirección debe ser recogida por una variable puntero: si no se recoge,
tendremos la memoria reservada pero no podremos acceder a ellas
porque no sabremos dónde ha quedado hecha esa reserva.

Como se ve, la dirección de devuelve con el tipo de dato void*. La


función malloc así lo hace porque está definida para hacer reserva de
bytes, al margen de para qué se reservan esos bytes. Pero no tendría
sentido trabajar con direcciones de tipo de dato void. Por eso, en la
asignación al puntero que debe recoger la dirección del array, se debe
indicar, mediante el operador forzar tipo, el tipo e la dirección.

Veamos un ejemplo:
long dim;
float *vector;
printf(“Dimensión de su vector ...”);

279
Fundamentos de informática. Programación en Lenguaje C

scanf(“%ld”,&dim);
vector = (float*)malloc(4 * dim);

En el ejemplo, hemos reservado tantos bytes como hace falta para


reservar memoria para un array de float de la dimensión indicada por el
usuario. Como cada variable float ocupa cuatro bytes hemos
multiplicado la dimensión por cuatro. Como se ve, en la asignación, a la
dirección de memoria que devuelve la función malloc, le hemos indicado
que deberá comportarse como dirección de float. De ese tipo es además
el puntero que la recoge. A partir de este momento, desde el puntero
vector y con operatoria de punteros podemos manejarnos por el array
creado en tiempo de ejecución.

También se puede recorrer el array con índices, como si de un vector


normal se tratase: la variable vector[i] es la misma que la variable
*(vector + i).

Es responsabilidad del programador reservar un número de bytes que


sea múltiplo del tamaño del tipo de dato para el que hacemos la
reserva. Si, por ejemplo, en una reserva para variables float, el número
de bytes no es múltiplo de 4, el compilador no interrumpirá su trabajo, y
generará el ejecutable; pero existe el peligro, en un momento dado, de
incurrir en una violación de memoria.

De forma habitual al invocar a la función malloc se hace utilizando el


operador sizeof. La sentencia anterior en la que reservábamos espacio
para nuestro vector de tipo float quedaría mejor de la siguiente forma:

vector = (float*)malloc(sizeolf(float) * dim);

Y una última observación sobre la reserva de la memoria. La función


malloc busca un espacio de memoria (en la memoria heap o montón:
pero no es ese tema que ahora vaya a ocuparnos) de la longitud que se
le indica en el argumento. Gracias a esta asignación de memoria se
podrá trabajar con una serie de valores, y realizar cálculos necesarios
para nuestra aplicación. Pero… ¿y si no hay en la memoria un espacio
suficiente de bytes consecutivos, libres y disponibles para satisfacer la

280
Capítulo 10. Asignación dinámica de memoria.

demanda? En ese caso, no podríamos realizar ninguna de las tareas que


el programa tiene determinadas, simplemente porque no tendríamos
memoria.

Cuando la función malloc lo logra satisfacer la demanda, devuelve el


puntero nulo. Es importante, siempre que se crea un espacio de
memoria con esta función, y antes de comenzar a hacer uso de ese
espacio, verificar que sí se ha logrado hacer buena reserva. En caso
contrario, habitualmente lo que habrá que hacer es abortar la ejecución
del programa, porque sin memoria, no hay datos ni capacidad de operar
con ellos.

Esta verificación se realiza de la siguiente manera:


vector = (float*)malloc(dimension * sizeof(float));
if(vector == NULL)
{
printf("\nNo hay memoria disponible.");
printf("\nEl programa va a terminar.");
printf("\nPulse cualquier tecla ... ");
exit(1);
}

Y así, nunca trabajaremos con un puntero cuya dirección es nula. Si no


hiciéramos esa verificación, en cuanto se echase mano del puntero
vector para recorrer nuestra memoria inexistente, el programa abortaría
inmediatamente. Es mejor tomar nosotros la iniciativa, mediante la
función exit, y decidir nosotros el modo de terminación del programa en
lugar de que lo decida un error fatal de ejecución.

Existen otras funciones muy similares de reserva de memoria dinámica.


Por ejemplo, la función calloc, que tiene el siguiente prototipo:

void *calloc(size_t nitems, size_t size);

Que recibe como parámetros el número de elementos que se van a


reservar y el número de bytes que ocupa cada elemento. La sentencia
anterior

vector = (float*)malloc(dimension * sizeof(float));

281
Fundamentos de informática. Programación en Lenguaje C

ahora, con la función calloc, quedaría:

vector = (float*)calloc(dimension, sizeof(float));

Como se ve, en sustancia, ambas funciones tienen un comportamiento


muy similar. También en este caso, obviamente, será conveniente hacer
siempre la verificación de que la memoria ha sido felizmente reservada.
Lo mejor es acudir a las ayudas de los compiladores para hacer buen
uso de las especificaciones de cada función.

Una última función de asignación dinámica de la memoria es la función


realloc. Su prototipo es el siguiente:

void *realloc(void *block, size_t size);

Esta función sirve para reasignar una zona de memoria sobre un


puntero. El primer parámetro es el del puntero sobre el que se va a
hacer el realojo; el segundo parámetro recoge el nuevo tamaño que
tendrá la zona de memoria reservada.

Si el puntero block ya tiene memoria asignada, mediante una función


malloc o calloc, o reasignada por otra llamada anterior realloc, entonces
la función varía el tamaño de la posición de memoria al nuevo tamaño
que indica la variable size. Si el tamaño es menor que el que había,
simplemente deja liberada para nuevos usos la memoria de más que
antes disponíamos y de la que hemos decidido prescindir. Si el nuevo
tamaño es mayor, entonces procura prolongar ese espacio reservado
con los bytes siguientes; si eso no es posible, entonces busca en la
memoria un espacio libre del tamaño indicado por size, y copia los
valores asignados en el tramo de memoria anterior en la nueva
dirección. La función devuelve la dirección donde queda ubicada toda la
memoria reservada. Es importante recoger esa dirección de memoria
que devuelve la función realloc, porque en su ejecución, la función
puede cambiar la ubicación del vector. La llamada podría ser así:

vector = (float*)realloc(vector, new_dim * sizeof(float));

282
Capítulo 10. Asignación dinámica de memoria.

Si el puntero block tiene el valor nulo, entonces realloc funciona de la


misma forma que la función malloc.

Si el valor de la variable size es cero, entonces la función realloc libera


el puntero block, que queda nulo. En ese caso, el comportamiento de la
función realloc es semejante al de la función free que vemos a
continuación.

Función free
Esta función viene también definida en la biblioteca stdlib.h. Su
cometido es liberar la memoria que ha sido reservada mediante la
función malloc, o calloc, o realloc. Su sintaxis es la siguiente:

void free(void *block);

Donde block es un puntero que tiene asignada la dirección de memoria


de cualquier bloque de memoria que haya sido reservada previamente
mediante una función de memoria dinámica, como por ejemplo la
función malloc ya presentada y vista en el epígrafe anterior.

La memoria que reserva el compilador ante una declaración de un


vector se ubica en la zona de variables de la memoria. La memoria que
se reserva mediante la función malloc queda ubicada en otro espacio de
memoria distinto, que se llama habitualmente heap. En ningún caso se
puede liberar, mediante la función free, memoria que haya sido creada
estática, es decir, vectores declarados como tales en el programa y que
reservan la memoria en tiempo de ejecución. Es esa memoria del heap
la que libera la función free. Si se aplica esa función sobre memoria
estática se produce un error en tiempo de compilación.

Ejemplo: la Criba de Erastóthenes


Supongamos que deseamos hacer un programa que almacene en un
vector todos los números primos menores que un millón. Para esa

283
Fundamentos de informática. Programación en Lenguaje C

búsqueda utilizaremos el algoritmo de la criba de Erastósthenes. Es un


algoritmo que permite encontrar todos los Números Primos menores o
iguales a un entero dado.

Se comienza generando una tabla con todos los números desde 1 hasta
el límite superior (en nuestro caso hemos quedado que un millón).
Tomamos el número 1 como primo por definición. A continuación se
pasa al siguiente número, que es el 2, que ya desde ese momento se
considerará primo, y se procede a marcar en la tabla como enteros
compuestos (es decir, no primos) a todos los números posteriores a 2 y
múltiplos de 2.

A continuación pasamos al siguiente número que no esté marcado como


compuesto, que resulta ser el 3, que queda considerado ahora como
primo, y procedemos a marcar como compuestos en nuestra tabla todos
los números posteriores a él y múltiplos suyos.

Ya hemos marcado como compuestos todos los múltiplos de 3. Ahora


buscamos el siguiente número no marcado como compuesto. El 4 es
múltiplo de 2 y ya ha quedado marcado como compuesto, así que nos lo
saltamos y llegamos al 5 que no es múltiplo ni de 2 ni de 3. El 5 queda
considerado primo y ahora procedemos a marcar como compuestos
todos los múltiplos de 5.

El proceso se va repitiendo hasta llegar al último número menor que el


límite superior marcado. Todos los números que no hayan sido
marcados como compuestos serán primos.

En realidad no es necesario realizar la criba hasta llegar al último


número menor que el límite superior fijado: basta llegar hasta la raíz
cuadrada de ese límite. La razón es que si un número no tiene un divisor
menor que su raíz cuadrada, entonces tampoco lo puede tener mayor
que su raíz cuadrada y, por tanto, si al llegar a ese limite no se ha
encontrado un divisor, entonces ese número es primo con total certeza.

284
Capítulo 10. Asignación dinámica de memoria.

Una vez tenemos claro el algoritmo que nos permitirá llegar a crear la
tabla de los primos, el siguiente paso será implementar el programa que
nos haga este proceso.

64. Escriba un programa que cree un vector con todos los primos
menores que un millón Utilice, para la búsqueda de los primos,
la criba de Erasthótenes.

Vamos a definir para ellos dos funciones más la función principal:


#include <stdio.h>
#include <math.h>
#include <stdlib.h>
#define MAX 1000000

long Criba(char*, long);


void TablaPrimos(char*, long*, long);

void main(void)
{
char *num;
long *primos;
long pr, i;

num = (char*)malloc((MAX + 1) * sizeof(char));


if(num == NULL)
{
printf("\nNo hay memoria disponible.");
printf("\nEl programa va a terminar.");
printf("\nPulse cualquier tecla ... ");
exit(1);
}
pr = Criba(num, MAX + 1);
// Ya está hecha la criba. Tenemos pr primos.
// Creamos ahora el vector que contendrá a los primos.
// Reservamos memoria para este vector:
primos = (long*)malloc((pr + 1) * sizeof(long));
if(primos == NULL)
{
printf("\nNo hay memoria disponible.");
printf("\nEl programa va a terminar.");
printf("\nPulse cualquier tecla ... ");
exit(1);

285
Fundamentos de informática. Programación en Lenguaje C

}
TablaPrimos(num, primos, MAX + 1);
free(num);

// Mostramos los primos por pantalla ...


printf("Primos menores que %ld... \n\n",MAX);
for(i = 0 ; *(primos + i) != 0 ; i++)
printf("%10ld", *(primos + i));
free(primos);
}

long Criba(char* num, long rango)


{
long i, j;
// En principio marcamos todos los elementos como PRIMOS
for(i = 0 ; i < rango; i++)
num[i] = 'p';
for(i = 2 ; i < sqrt(rango) ; i++)
if(num[i] == 'p')
for(j = 2 * i ; j < rango ; j += i)
num[j] = 'c';
for( i = 1 , j = 0 ; i < rango ; i++)
if(num[i] == 'p') j++;
return j;
}

void TablaPrimos(char* num, long* primos, long rango)


{
long i, j;
for(i = 1 , j = 0 ; i < rango ; i++)
if(num[i] == 'p')
{
*(primos + j) = i;
j++;
}
*(primos + j) = 0;
}

Vamos viendo este programa, resuelto mediante funciones. Primero


reservamos, en la función main, un espacio de memoria suficiente para
albergar tantas variables char (de un bytes cada una) como indica MAX
+ 1. MAX es la macro que recoge el límite superior sobre el que se van a
buscar todos los primos inferiores. A esa memoria, que manejaremos
desde un puntero tipo char* que hemos llamado num, le asignamos a
todas sus posiciones el valor ‘p’, que vamos a entender que significa
“primo”. Y es sobre ese vector sobre quien se hace la criba de

286
Capítulo 10. Asignación dinámica de memoria.

Erastóthenes, definida en la función Criba, que devuelve el número de


primos que hay en ese rango entre 1 y MAX (la función principal recoge
ese valor en la variable pr) y que deja modificada la memoria recogida
por el puntero num: una vez ejecutada la función Criba, cada posición
valdrá ‘c’ ó ‘p’ según que su índice en el vector sea compuesto o primo:
num[i] valdrá ‘p’ si i es primo, y valdrá ‘c’ si i en compuesto.

Como ya sabemos cuantos primos hay en nuestro intervalo, ahora


creamos un espacio de memoria para almacenar enteros largos: tantos
como indica la variable pr (en realidad reservamos uno más que pr, por
el motivo que se explica enseguida). Y llamando a la función
TablaPrimos se le asigna a cada posición de esa memoria cada uno de
los pr primos del intervalo.

El último valor (ese espacio en memoria de más que acabamos de


reservar) de nuestro vector de primos lo ponemos a cero: así el
recorrido de nuestro vector no se rige por un índice, sino por un valor
que hace de fin de lista: se podrá recorrer el vector de enteros primos
mientras que no se llegue a un valor cero. Así se ha recorrido al final de
función principal, cuando se muestra la lista de primos por pantalla.

Si en el programa se quiere aumentar el rango de primos bastará


modificar la definición de la directiva define.

Al final del programa, y antes de terminar su ejecución liberamos la


memoria de la tabla de primos. Antes, ya habíamos liberado la memoria
recogida por el puntero num. Y eso es algo interesante a destacar de la
memoria dinámica: se crea cuando se necesita, y se libera cuando ya no
se necesita. Y en ese aspecto, esta memoria tiene un régimen de vida y
de ámbito diferente al de las variables creadas por declaración. La
variable puede dejar de existir antes de finalizar el bloque en el que ha
sido creada; y puede comenzar a existir pasado un tiempo al inicio de la
ejecución de su bloque.

287
Fundamentos de informática. Programación en Lenguaje C

Matrices en memoria dinámica


Para crear matrices, podemos trabajar de dos maneras diferentes. La
primera es crear una estructura similar a la indicada en el cuadro 12.1.
Presentamos un código que genera una matriz de tipo float. El
programa solicita al usuario las dimensiones de la matriz (el número de
filas y el número de columnas). A continuación la aplicación reserva un
array de tantos punteros como filas tenga la matriz; y sobre cada uno
de esos punteros hace una reserva de tantos elementos float como
columnas se deseen en la matriz.

Luego, para ver cómo se puede hacer uso de esa matriz creada
mediante memoria dinámica, solicitamos al usuario que dé valor a cada
elemento de la matriz y, finalmente, mostramos por pantalla todos los
elementos de la matriz.

Por último, se realiza la liberación de la memoria. Para ello primero


habrá que liberar la memoria asignada a cada uno de los vectores
creados en memoria dinámica sobre el puntero p, y posteriormente
liberar al puntero p del array de punteros.

El código es el siguiente:
#include <stdlib.h>
#include <stdio.h>

void main(void)
{
float **p;
short f, c;
short i, j;
// Introducir las dimensiones ...
printf("Indique filas del vector ... ");
scanf("%hd",&f);
printf("Indique columnas del vector ... ");
scanf("%hd",&c);
// Creación de las filas ...
p = (float**)malloc(f * sizeof(float*));
if(p == NULL)
{
printf("Memoria insuficiente.\n");
printf("La ejección se interrumpirá.\n");
printf("Pulse una tecla para terminar ... ");

288
Capítulo 10. Asignación dinámica de memoria.

getchar();
exit(0);
}
// Creación de las columnas ...
for( i = 0 ; i < f ; i++)
{
*(p + i) = (float*)malloc(c * sizeof(float));
if(*(p + i) == NULL)
{
printf("Memoria insuficiente.\n");
printf("La ejección se interrumpirá.\n");
printf("Pulse una tecla para terminar...");
getchar();
exit(0);
}
}
// Asignación de valores ...
for(i = 0 ; i < f ; i++)
for(j = 0 ; j < c ; j++)
{
printf("matriz[%2hd][%2hd] = ", i, j);
scanf("%f",*(p + i) + j);
}
// Mostrar la matriz por pantalla ...
for(i = 0 ; i < f ; i++)
{
printf("\n");
for(j = 0 ; j < c ; j++)
printf("%6.2f\t",*(*(p + i) + j));
}
// Liberar la memoria ...
for(i = 0 ; i < f ; i++)
free(*(p + i));
free(p);
}

Desde luego, hemos trabajado con operatoria de punteros. Ya se explicó


que hablar de *(*(p + i) + j) es lo mismo que hablar de p[i][j]. Y que
hablar de (*(p + i) + j) es hablar de &p[i][j].

Y así se podría haber codificado. Veamos algunas de las líneas del


código anterior, recogidas con operatoria de índices:
// Asignación de valores ...
for(i = 0 ; i < f ; i++)
for(j = 0 ; j < c ; j++)
{
printf("matriz[%2hd][%2hd] = ", i, j);
scanf("%f",&p[i][j]);
}

289
Fundamentos de informática. Programación en Lenguaje C

// Mostrar la matriz por pantalla ...


for(i = 0 ; i < f ; i++)
{
printf("\n");
for(j = 0 ; j < c ; j++)
printf("%6.2f\t",p[i][j]);
}

Una última observación a este código presentado: el puntero p es (debe


ser así) puntero a puntero. Y, efectivamente, él apunta a un array de
punteros que, a su vez, apuntan a un array de elementos float.

Decíamos que había dos formas de crear una matriz por asignación
dinámica de memoria. La segunda es crear un solo array, de longitud
igual al producto de filas por columnas. Y si la matriz tiene α filas y β
columnas, considerar los β primeros elementos del vector como la
primera fila de la matriz; y los segundos β elementos, como la segunda
fila, y así, hasta llegar a la última fila.

El código de esta nueva forma de manejar matrices podría ser:


#include <stdlib.h>
#include <stdio.h>
void main(void)
{
float *p;
short f, c;
short i, j;
// Introducir las dimensiones ...
printf("Indique filas del vector ... ");
scanf("%hd",&f);
printf("Indique columnas del vector ... ");
scanf("%hd",&c);
// Creación de la matriz ...
p = (float*)malloc(f * c * sizeof(float*));
if(p == NULL)
{
printf("Memoria insuficiente.\n");
printf("La ejección se interrumpirá.\n");
printf("Pulse una tecla para terminar ... ");
getchar();
exit(0);
}
// Asignación de valores ...
for(i = 0 ; i < f ; i++)
for(j = 0 ; j < c ; j++)
{

290
Capítulo 10. Asignación dinámica de memoria.

printf("matriz[%2hd][%2hd] = ", i, j);


scanf("%f",p + (i * c + j));
}
// Mostrar la matriz por pantalla ...
for(i = 0 ; i < f ; i++)
{
printf("\n");
for(j = 0 ; j < c ; j++)
printf("%6.2f\t",*(p + (i * c + j)));
}
// Mostrar los datos como vector lineal ...
printf("\n\n");
for(i = 0 ; i < f * c ; i++)
printf("%6.2f\t",*(p + i));
// Liberar la memoria ...
free(p);
}

Ahora el puntero es un simple puntero a float. Y jugamos con los valores


de los índices para avanzar más o menos en el array. Cuando hablamos
de *(p + (i * c + j)), donde p es el puntero, i es el contador de filas, j el
contador de columnas, y c la variable que indica cuántas columnas hay,
estamos recorriendo el array de la siguiente manera: si queremos ir, por
ejemplo, a la fila 2 (i = 2) y a la columna 5 (j = 5), y suponiendo que la
matriz tiene, por ejemplo, 8 columnas (c = 8) entonces, ese elemento
del vector (2, 5) está ubicado en la posición 2 * 8 + 5. es decir, en la
posición 21.

Con el valor i = 0 tenemos los elementos de la primera fila, situados en


el vector desde su posición 0 hasta el su posición c – 1. Con el valor i =
1 tenemos los elementos de la segunda fila, situados en el vector desde
su posición c hasta su posición 2 * c – 1. Y en general, la fila i se sitúa
en el vector desde la posición i * c hasta la posición (i + 1) * c – 1.

De nuevo, podremos trabajar con operatoria de índices: hablar de *(p +


(i * c + j)) es lo mismo que hablar del elemento del vector p[i * c + j].

291
Fundamentos de informática. Programación en Lenguaje C

Ejercicios.

65. Un cuadro mágico es un reticulado de n filas y n columnas que


tiene la propiedad de que todas sus filas, y todas sus columnas,
y las diagonales principales, suman el mismo valor. Por
ejemplo:

6 1 8

7 5 3

2 9 4

La técnica que se utiliza para generar cuadros mágicos (que


tienen siempre una dimensión impar: impar número de filas y
de columnas) es la siguiente:

a. Se comienza fijando el entero 1 en el espacio central de la


primera fila.

b. Se van escribiendo los sucesivos números (2, 3, ...)


sucesivamente, en las casillas localizadas una fila arriba y
una columna a la izquierda. Estos desplazamientos se
realizan tratando a la matriz como si estuviera envuelta
sobre sí misma, de forma que moverse una posición hacia
arriba desde la fila superior lleva a la fila inferior, y moverse
una posición a la izquierda desde la primera columna lleva a
la columna más a la derecha del cuadro.

c. Si se llega a una posición ya ocupada (es decir, si arriba a la


izquierda ya está ocupado con un número anterior),
entonces la posición a rellenar cambia, que ahora será la
inmediatamente debajo de la última casilla rellenada.
Después se continúa el proceso tal y como se ha descrito en

292
Capítulo 10. Asignación dinámica de memoria.

el punto anterior.

Escriba un programa que genere el cuadro mágico de la


dimensión que el usuario desee, y lo muestre luego por
pantalla.

/* ===== PROGRAMA DEL CUADRO MÁGICO.====================== */

#include <stdio.h>
#include <stdlib.h>

// Para comprender estas cuatro instrucciones consultar


// capítulo 11.

typedef unsigned long int uli;


typedef unsigned short int usi;
typedef signed long int sli;
typedef signed short int ssi;

usi Dimension(void);
void CuadroACero(uli**,usi);
uli** AsignarMemoria(uli**,usi);
void CrearCuadro(uli**,usi);
void MostrarCuadro(uli**, usi);

void main(void)
{
usi dim;
uli **cuadro;
usi i;

do
{
// Valor de la dimensión ...
dim = Dimension();
if(!dim) break;

// Asignación de memoria ...


cuadro = AsignarMemoria(cuadro,dim);
if(cuadro == NULL) break;
// Inicializamos la matriz a cero...
CuadroACero(cuadro,dim);

// Asignar valores a los elementos del cuadro mágico...


CrearCuadro(cuadro,dim);

293
Fundamentos de informática. Programación en Lenguaje C

// Mostrar el cuadro mágico...


MostrarCuadro(cuadro,dim);
printf("\n\nPulse una tecla para mostrar
otro cuadro ... ");
getchar();

// Liberar la memoria reservada

for(i = 0 ; i < dim ; i++) free(*(cuadro + i));


free(cuadro);
}while(dim);
}

/* ------------------------------------------------------- */
/* Función Dimension() */
/* ------------------------------------------------------- */

usi Dimension(void)
{
usi d;
do
{
clrscr();
printf("Dimension del cuadro. Debe ser un
valor IMPAR ... ");
printf("\nIndique dimension CERO si
desea terminar la aplicacion -> ");
scanf("%hu",&d);
}while(!(d % 2) && d);
return d;
}

/* ------------------------------------------------------- */
/* Función CuadroACero() */
/* ------------------------------------------------------- */

void CuadroACero(uli**C,usi d)
{
usi i, j;
for(i = 0 ; i < d ; i++)
for(j = 0 ; j < d ; j++)
*(*(C + i) + j) = 0;
}

/* ------------------------------------------------------- */
/* Función AsignarMemoria() */
/* ------------------------------------------------------- */

294
Capítulo 10. Asignación dinámica de memoria.

uli** AsignarMemoria(uli**C,usi d)
{
usi i;
if((C = (uli**)malloc(d * sizeof(uli*))) == NULL)
{
printf("\nError (1) de asignacion de
memoria.");
printf("\nLa ejecucion del programa no
puede continuar.");
printf("\nPulse cualquier tecla para terminar
la aplicacion");
getchar();
return C;
}

for(i = 0 ; i < d ; i++)


if((*(C + i)=(uli*)malloc(d*sizeof(uli)))==NULL)
{
printf("\nError (2) de asignación de
memoria.");
printf("\nLa ejecución del programa no
puede continuar.");
printf("\nPulse cualquier tecla para
terminar la aplicacion");
getchar();
C = NULL;
}

return(C);
}

/* ------------------------------------------------------- */
/* Función CrearCuadro() */
/* ------------------------------------------------------- */

void CrearCuadro(uli**C,usi d)
{
usi posX, posY, antX, antY, elem;

// Posición inicial: el centro de la primera fila...


posX = d / 2;
posY = 0;
elem = 1;

while(elem <= d * d)
{
*(*(C + posX) + posY) = elem;
// Nueva posición X ...
antX = posX;
posX = posX ? posX - 1 : d - 1;
// Nueva posicion Y ...

295
Fundamentos de informática. Programación en Lenguaje C

antY = posY;
posY = posY ? posY - 1 : d - 1;
// Si la casilla ya ha sido ocupada ...
if(*(*(C + posX) + posY))
{
posX = antX;
posY = antY == d - 1 ? 0 : antY + 1;
}
elem++;
}
}

/* ------------------------------------------------------- */
/* Función MostrarCuadro() */
/* ------------------------------------------------------- */

void MostrarCuadro(uli**C, usi d)


{
uli *sumaf,*sumac,sumad[2];
usi i, j;

sumac = (uli*)malloc(d * sizeof(uli));


sumaf = (uli*)malloc(d * sizeof(uli));

if(sumaf != NULL)
{
for(i = 0 ; i < d ; i++)
{
*(sumaf + i) = 0;
for(j = 0 ; j < d ; j++)
*(sumaf + i) += *(*(C + i) + j);
}
}

if(sumac != NULL)
{
for(i = 0 ; i < d ; i++)
{
*(sumac + i) = 0;
for(j = 0 ; j < d ; j++)
*(sumac + i) += *(*(C + j) + i);
}
}

sumad[0] = sumad[1] = 0;
for(i = 0 ; i < d ; i++)
sumad[0] += *(*(C + i) + i);
for(i = 1 ; i <= d ; i++)
sumad[1] += *(*(C + d - i) + i - 1);

for(i = 0 ; i < d ; i++)

296
Capítulo 10. Asignación dinámica de memoria.

{
printf("\n");
for(j = 0 ; j < d ; j++)
printf("%5hu",*(*(C + j) + i));
printf(" -> %lu\n",*(sumaf + i));
}

printf("\n");
for(i = 0 ; i < d ; i++)
printf(" |");
printf("\n");
for(i = 0 ; i < d ; i++)
printf(" V");
printf("\n\n");
for(i = 0 ; i < d ; i++)
printf("%5lu",*(sumac + i));

printf("\n\nSuma Diagonal principal .... %5lu",


sumad[0]);
printf("\n\nSuma Diagonal secundaria ... %5lu",
sumad[1]);

297
Fundamentos de informática. Programación en Lenguaje C

298
CAPÍTULO 11
ALGUNOS USOS CON FUNCIONES

En un capítulo anterior hemos visto lo básico sobre funciones. Con todo


lo dicho en ese tema se puede trabajar perfectamente en C, e
implementar multitud de programas, con buena modularidad.

En este tema queremos presentar muy brevemente algunos usos más


avanzados de las funciones: distintas maneras en que pueden ser
invocadas. Punteros a funciones, vectores de punteros a funciones, el
modo de pasar una función como argumento de otra función. Son
modos de hacer sencillos, que añaden, a todo lo dicho en el tema
anterior, posibilidades de diseño de programas.

Otra cuestión que abordaremos en este tema es cómo definir aquellas


funciones de las que desconozcamos a priori el número de parámetros
que han de recibir. De hecho, nosotros ya conocemos algunas de esas
funciones: la función printf puede ser invocada con un solo parámetro
(la cadena de caracteres que no imprima ningún valor) o con tantos
como se quiera: tantos como valores queramos que se impriman en
Fundamentos de informática. Programación en Lenguaje C

nuestra cadena de caracteres. Veremos también aquí la manera de


definir funciones con estas características.

Punteros a funciones
En los primeros temas de este manual hablábamos de que toda la
información de un ordenador se guarda en memoria. No sólo los datos.
También las instrucciones tienen su espacio de memoria donde se
almacenan y pueden ser leídas. Todo programa debe ser cargado sobre
la memoria principal del ordenador antes de comenzar su ejecución.

Y si una función cualquiera tiene una ubicación en la memoria, entonces


podemos hablar de la dirección de memoria de esa función. Desde
luego, una función ocupará más de un byte, pero se puede tomar como
dirección de memoria de una función aquella donde se encuentra la
entrada de esa función.

Y si tengo definida la dirección de una función… ¿No podré definir un


puntero que almacene esa dirección? La respuesta es que sí, y ahora
veremos cómo poder hacerlo. Por tanto, podremos usar un puntero para
ejecutar una función. Ese puntero será el que también nos ha de
permitir poder pasar una función como argumento de otra función.

La declaración de un puntero a una función es la declaración de una


variable. Ésta puede ser local, y de hecho, como siempre, será lo
habitual. Cuando se declara un puntero a función para poder asignarle
posteriormente la dirección de una o u otra función, la declaración de
ese puntero a función debe tener un prototipo coincidente con las
funciones a las que se desea apuntar.

Supongamos que tenemos las siguientes funciones declaradas al inicio


de un programa:

tipo_función nombre_función_1 (tipo1, …, tipoN);

tipo_función nombre_función_2 (tipo1, …, tipoN);

300
Capítulo 11. Algunos usos con funciones.

Y supongamos ahora que queremos declarar, por ejemplo en la función


principal, un puntero que pueda recoger la dirección de estas dos
funciones. La declaración del puntero será la siguiente:

tipo_función (*puntero_a_funcion)(tipo1,…,tipoN);

De esta declaración podemos hacer las siguientes importantes


observaciones:

1. tipo_función debe coincidir con el tipo de la función a la que va a


apuntar el puntero a función. De la misma manera la lista de
argumentos debe ser coincidente, tanto en los tipos de dato que
intervienen como en el orden. En definitiva, los prototipos de la
función y de puntero deber ser idénticos.

2. Si *puntero_a_función NO viniese recogido entre paréntesis


entonces no estaríamos declarando un puntero a función, sino una
función normal que devuelve un tipo de dato puntero: un puntero
para una recoger la dirección de una variable de tipo tipo_función.
Por eso los paréntesis no son opcionales.

Una vez tenemos declarado el puntero, el siguiente paso será siempre


asignarle una dirección de memoria. En ese caso, la dirección de una
función. La sintaxis para esta asignación es la siguiente:

puntero_a_función = nombre_función_1;

Donde nombre_función_1 puede ser el nombre de cualquier función


cuyo prototipo coincide con el del puntero.

Una observación importante: al hacer la asignación de la dirección de la


función, hacemos uso del identificador de la función: no se emplea el
operador &; tampoco se ponen los paréntesis al final del identificador de
la función.

Al ejecutar puntero_a_funcion obtendremos un comportamiento idéntico


al que tendríamos si ejecutáramos directamente la función. La sintaxis
para invocar a la función desde el puntero es la siguiente:

301
Fundamentos de informática. Programación en Lenguaje C

resultado = (*puntero_a_función)(var_1, …,var_N);

Y así, cuando en la función principal se escriba esta sentencia tendremos


el mismo resultado que si se hubiera consignado la sentencia

resultado = nombre_a_función_1(var_1, …,var_N);

Antes de ver algunos ejemplos, hacemos una última observación. El


puntero función es una variable local en una función. Mientras estemos
en el ámbito de esa función podremos hacer uso de ese puntero. Desde
luego toda función trasciende el ámbito de cualquier otra función; pero
no ocurre así con los punteros.

Veamos algún ejemplo. Hacemos un programa que solicita al usuario


dos operandos y luego si desea sumarlos, restarlos, multiplicarlos o
dividirlos. Entonces muestra el resultado de la operación. Se definen
cuatro funciones, para cada una de las cuatro posibles operaciones a
realizar. Y un puntero a función al que se le asignará la dirección de la
función que ha de realizar esa operación seleccionada.

El código podría quedar como sigue:


#include <stdio.h>

float sum(float, float);


float res(float, float);
float pro(float, float);
float div(float, float);

void main(void)
{
float a, b;
unsigned char op;
float (*operacion)(float, float);

printf("Primer operador ... ");


scanf("%f",&a);
printf("Segundo operador ... ");
scanf("%f",&b);
printf("Operación ( + , - , * , / ) ... ");
do
op = getchar();
while(op !='+' && op !='-' && op !='*' && op !='/');

switch(op)

302
Capítulo 11. Algunos usos con funciones.

{
case '+': operacion = sum; break;
case '-': operacion = res; break;
case '*': operacion = pro; break;
case '/': operacion = div;
}

printf("\n%f %c %f = %f",a, op, b, (*operacion)(a, b));


}

float sum(float x, float y)


{ return x + y; }

float res(float x, float y)


{ return x - y; }

float pro(float x, float y)


{ return x * y; }

float div(float x, float y)


{ return y ? x / y : 0; }

La definición de las cuatro funciones no requiere a estas alturas


explicación alguna. El puntero operación queda definido como variable
local dentro de main. Dependiendo del valor de la variable op al puntero
se le asignará la dirección de una de las cuatro funciones, todas ellos
con idéntico prototipo, igual a su vez al prototipo del puntero.

Evidentemente, esto es sólo un ejemplo. Hay otras muchas formas de


resolver el problema, y quizá alguno piense que es más complicado el
uso del puntero, y que podría hacerse recogido en cada case de la
estructura switch la llamada a la función correspondiente. Y no le
faltará razón. Ya hemos dicho muchas veces que aquí tan hay tantas
soluciones válidas como programadores. Pero desde luego las
posibilidades de implementación que ofrece el puntero a función son
claras.

Vectores de punteros a funciones


No aportamos aquí ningún concepto nuevo, sino una reflexión sobre otra
posibilidad que ofrece el tener punteros a funciones.

303
Fundamentos de informática. Programación en Lenguaje C

Como todo puntero, un puntero a función puede formar parte de un


array. Y como podemos definir arrays de todos los tipos que queramos,
entonces podemos definir un array de tipo de dato punteros a funciones.
Todos ellos serán del mismo tipo, y por tanto del mismo prototipo de
función. La sintaxis de definición será la siguiente:

tipo_función (*puntero_a_función[dimensión])(tipo1, … tipoN);

Y la asignación puede hacerse directamente en la creación del puntero,


o en cualquier otro momento:
tipo_función (*puntero_a_función[n])(tipo1, … tipoN) =
{ funcion_1, función_2, …, función_n }

Donde deberá haber tantos nombres de función, todas ellas del mismo
tipo, como indique la dimensión del vector. Como siempre, cada una de
las funciones deberá quedar declarada y definida en el programa.

El vector de funciones se emplea de forma análoga a cualquier otro


vector. Se puede acceder a cada una de esas funciones mediante
índices, o por operatoria de punteros.

Podemos continuar con el ejemplo del epígrafe anterior. Supongamos


que la declaración del puntero queda transformada en la declaración de
una array de dimensión 4:

float(*operacion[4])(float,float)= {sum,res,pro,div};

Con esto hemos declarado cuatro punteros, cada uno de ellos apuntando
a cada una de las cuatro funciones definidas. A partir de ahora será lo
mismo invocar a la función sumaf que invocar a la función apuntada por
el primer puntero del vector.

Si incorporamos en la función main la declaración de una variable i de


tipo entero, la estructura switch puede quedar ahora como sigue
switch(op)
{
case '+': i = 0; break;
case '-': i = 1; break;
case '*': i = 2; break;
case '/': i = 3;

304
Capítulo 11. Algunos usos con funciones.

Y ahora la ejecución de la función será como sigue:

printf(“\n\n%f %c %f = %f”, a, op, b, (*operación[i])(a, b));

Funciones como argumentos


Se trata ahora de ver cómo hemos de definir un prototipo de función
para que pueda recibir a otras funciones como parámetros. Un programa
que usa funciones como argumentos suele ser difícil de comprender y de
depurar, pero se adquiere a cambio una gran potencia en las
posibilidades de C.

La utilidad de pasar funciones como parámetro en la llamada a otra


función está en que se puede hacer depender cuál sea la función a
ejecutar del estado a que se haya llegado a lo largo de la ejecución.
Estado que no puede prever el programador, porque dependerá de cada
ejecución concreta. Y así, una función que recibe como parámetro la
dirección de una función, tendrá un comportamiento u otro según reciba
la dirección de una u otra de las funciones declaradas y definidas.

La sintaxis del prototipo de una función que recibe como parámetro la


dirección de otra función es la habitual: primero el tipo de la función,
seguido de su nombre y luego, entre paréntesis, la lista de parámetros.
Y entre esos parámetros uno o algunos pueden ser punteros a
funciones. La forma en que se indica ese parámetro en la lista de
parámetros es la siguiente:

tipo_función (*puntero_a_funcion)(parámetros)

(Lo que queda aquí recogido no es el prototipo de la función, sino el


modo en que se consigna el parámetro puntero a función dentro de una
lista de parámetros en un prototipo de función que recibe, entre sus
argumentos, la dirección de una función.)

305
Fundamentos de informática. Programación en Lenguaje C

Supongamos que este parámetro pertenece al prototipo de la función


nombre_función. Entonces cuando se compile nombre_función el
compilador sólo sabrá que esta función recibirá como argumento, entre
otras cosas, la dirección de una función que se ajusta al prototipo
declarado como parámetro. Cuál sea esa función es cuestión que no se
conocerá hasta el momento de la ejecución y de la invocación a esa
función.

La forma en que se llamará a la función será la lógica de acuerdo con


estos parámetros. El nombre de la función y seguidamente, entre
paréntesis, todos sus parámetros en el orden correcto. En el momento
de recoger el argumento de la dirección de la función se hará de la
siguiente forma:

*puntero_a_función(parámetros)

De nuevo será conveniente seguir con el ejemplo anterior, utilizando


ahora una quinta función para realizar la operación y mostrar por
pantalla su resultado:
#include <stdio.h>
#include <conio.h>

float sum(float, float);


float res(float, float);
float pro(float, float);
float div(float, float);
void mostrar(float, char, float, float(*f)(float, float));

void main(void)
{
float a, b;
unsigned char op;
float (*operacion[4])(float, float) ={sum,res,pro,div};
do
{
printf("\n\nPrimer operador ... ");
scanf("%f",&a);
printf("Segundo operador ... ");
scanf("%f",&b);
printf("Operación ... \n)");
printf("\n\n1. Suma\n2. Resta”);
printf(“\n3. Producto\n4. Cociente");
printf("\n\n\tSu opción (1 , 2 , 3 , 4) ... ");

306
Capítulo 11. Algunos usos con funciones.

do
op = getche();
while(op - '0' < 1 || op - '0' > 4 );
mostrar(a,op,b,operacion[(short)(op - '1')]);
printf("\n\nOtra operación (s / n) ... ");
do
op = getche();
while(op != 's' && op != 'n');
}while(op == 's');
}

float sum(float x, float y)


{ return x + y; }

float res(float x, float y)


{ return x - y; }

float pro(float x, float y)


{ return x * y; }

float div(float x, float y)


{ return y ? x / y : 0; }

void mostrar(float x,char c,float y,float(*f)(float,float))


{
if(c == '1') c = '+';
else if(c == '2') c = '-';
else if(c == '3') c = '*';
else c = '/';

printf("\n\n%f %c %f = ", x, c, y);


printf("%f.", (*f)(x,y));
}

Vamos viendo poco a poco el código. Primero aparecen las declaraciones


de cinco funciones: las encargadas de realizar suma, resta, producto y
cociente de dos valores float. Y luego, una quinta función, que hemos
llamado mostrar, que tiene como cuarto parámetro un puntero a
función. La declaración de este parámetro es como se dijo: el tipo del
puntero de función, el nombre del puntero, recogido entre paréntesis y
precedido de un asterisco, y luego, también entre paréntesis, la lista de
parámetros del puntero a función. Así ha quedado declarada.

Y luego comienza la función principal, main, donde viene declarado un


vector de cuatro punteros a función. A cada uno de ellos le hemos
asignado una de las cuatro funciones.

307
Fundamentos de informática. Programación en Lenguaje C

Y hemos recogido el código de toda la función main en un bloque do–


while, para que se realicen tantas operaciones como se deseen. Cada
vez que se indique una operación se hará una llamada a la función
mostrar que recibirá como parámetro una de las cuatro direcciones de
memoria de las otras cuatro funciones. La llamada es de la forma:
mostrar(a,op,b,operacion[(short)(op - '1')]);

Donde el cuarto parámetro es la dirección de la operación


correspondiente. operacion[0] es la función sum; operacion[1] es la
función res; operacion[2] es la función pro; y operacion[3] es la función
div. El valor de op – 1 será 0 si op es el carácter ‘1’; será 1 si es el
carácter ‘2’; será 2 si es el carácter ‘3’; y será 3 si es el carácter ‘4’.

Y ya estamos en la función mostrar, que simplemente tiene que ejecutar


el puntero a función y mostrar el resultado por pantalla.

Ejemplo: la función qsort


Hay ejemplos de uso de funciones pasadas como parámetros muy
utilizados, como por ejemplo la función qsort, de la biblioteca stdlib.h.
Esta función es muy eficaz en la ordenación de grandes cantidades de
valores. Su prototipo es:

void qsort(void *base, size_t nelem, size_t width, int


(*fcmp)(const void*, const void*));

Es una función que no devuelve valor alguno. Recibe como parámetros


el puntero base que es quien recoge la dirección del array donde están
los elementos a ordenar; nelem, que es un valor entero que indica la
dimensión del vector pasado como primer parámetro; width es el tercer
parámetro, que indica el tamaño que tiene cada uno de los elementos
del array; y por fin, el cuarto parámetro, es una función que devuelve
un valor entero y que recibe como parámetros dos direcciones de dos
variables. La función que se pase como parámetro en este puntero debe
devolver un 1 si su primer parámetro apunta a un valor mayor que el

308
Capítulo 11. Algunos usos con funciones.

segundo parámetro; el valor -1 si es al contrario; el valor 0 si el valor de


ambos parámetros son iguales.

Hay que explicar porqué los tipos que recoge el prototipo son siempre
void. El motivo es porque la función qsort está definida para ser capaz
de ordenar un array de cualquier tipo. Puede ordenar enteros, reales,
letras, u otros tipos de dato mucho más complejos, que se pueden crear
y que veremos en un capítulo posterior. La función no tiene en cuenta el
tipo de dato: simplemente quiere saber dos cosas:

1. El tamaño del tipo de dato; y eso se le facilita a la función a través


del tercer parámetro, width.

2. Cómo se define la ordenación: como a priori no se sabe el tipo de


dato, tampoco puede saber la función qsort con qué criterio decidir
qué valores del dominio del tipo de dato son mayores, o iguales, o
menores. Por eso, la función qsort requiere que el usuario le facilite,
mediante una función muy simple que debe implementar cada
usuario de la función qsort, ese criterio de ordenación.

Actualmente el algoritmo que da soporte a la función qsort es el más


eficaz en las técnicas de ordenación de grandes cantidades de valores.

Vamos a ver un ejemplo de uso de esta función. Vamos a hacer un


programa que ordene un vector bastante extenso de valores enteros
que asignaremos de forma aleatoria. Para ello deberemos emplear
también alguna función de generación de aleatorios. Pero esa es
cuestión muy sencilla que aclaramos antes de mostrar el código de la
función que hemos sugerido.

Existe una función en stdlib.h llamada random. Esa función pretende


ser un generador de números aleatorios. Es un generador bastante
malo, pero para nuestros propósitos sirve. Su prototipo es:

int random(int num);

309
Fundamentos de informática. Programación en Lenguaje C

Es una función que devuelve un entero aleatorio entre 0 y (num − 1) . El


valor de num que se le pasa a la función como parámetro también debe
ser un valor entero.

Cuando en una función se hace uso de la función random, antes debe


ejecutarse otra función previa: la función randomize. Esta función
inicializa el generador de aleatorios con un valor inicial también
aleatorio. Su prototipo es:

void randomize(void);

Y se ejecuta en cualquier momento del programa, pero siempre antes de


la primera vez que se ejecute la función random.

Una vez presentadas las funciones necesarias para general aleatorios,


veamos como queda un posible programa que genera una serie de
enteros aleatorios y que los ordena de menor a mayor, haciendo uso de
la función qsort:
#include <stdio.h>
#include <stdlib.h>

#define TAM 10
#define RANGO 1000
int ordenar(void*,void*);

void main(void)
{
long numeros[TAM];
long i;
randomize();
for(i = 0 ; i < TAM ; i++)
numeros[i] = random(RANGO);

// Vamos a ordenar esos numeros ...

qsort((void*)numeros, TAM, sizeof(long), ordenar);

// Mostramos resultados

for(i = 0 ; i < TAM ; i++)


printf("numeros[%4ld] = %ld\n", i, numeros[i]);
}

310
Capítulo 11. Algunos usos con funciones.

// La función de ordenación ...


int ordenar(void *a, void *b)
{
if(*(long*)a > *(long*)b)
return 1;
else if(*(long*)a < *(long*)b)
return -1;
return 0;
}

Hemos definido la función ordenar con un prototipo idéntico al exigido


por la función qsort. Recibe dos direcciones de memoria (nosotros
queremos que sea de enteros largos, pero eso no se le puede decir a
qsort) y resuelve cómo discernir la relación mayor que, menor que, o
identidad entre dos cualesquiera de esos valores que la función recibirá
como parámetros.

La función trata a las dos direcciones de memoria como de tipo de dato


void. El puntero a void ni siquiera sabe qué cantidad de bytes ocupa la
variable a la que apunta. Toma la dirección del byte primero de nuestra
variable, y no hace más. Dentro de la función, el código ya especifica,
mediante el operador forzar tipo, que la variable apuntada por esos
punteros será tratada como una variable long. Es dentro de nuestra
función donde especificamos el tipo de dato de los elementos que vamos
a ordenar. Pero la función qsort van a poder usarla todos aquellos que
tengan algo que ordenar, independientemente de qué sea ese “algo”:
porque todo el que haga uso de qsort le explicará a esa función, gracias
al puntero a funciones que recibe como parámetro, el modo en que se
decide quien va antes y quien va después. Lo que aporta qsort es la
rapidez en poner en orden una cantidad ingente de valores del mismo
tipo.

Y, efectivamente, hay muchas formas de resolver los problemas y de


implementarlos. Y el uso de punteros a funciones, o la posibilidad de
pasar como parámetro de una función la dirección de memoria de otra
función es una posibilidad que ofrece enormes ventajas y posibilidades.

311
Fundamentos de informática. Programación en Lenguaje C

Estudio de tiempos
A veces es muy ilustrativo poder estudiar la velocidad de algunas
aplicaciones que hayamos implementado en C.

En algunos programas de ejemplo de capítulos anteriores habíamos


presentado un programa que ordenaba cadenas de enteros. Aquel
programa, que ahora mostraremos de nuevo, estaba basado en un
método de ordenación llamado método de la burbuja: consiste en ir
pasando para arriba aquellos enteros menores, de forma que van
quedando cada vez más abajo, o más atrás (según se quiera) los
enteros mayores. Por eso se llama el método de la burbuja: porque lo
liviano “sube”.

Vamos a introducir una función que controla el tiempo de ejecución. Hay


funciones bastante diversas para este estudio. Nosotros nos vamos
ahora a centrar en una función, disponible en la biblioteca time.h,
llamada clock, cuyo prototipo es:

clock_t clock(void);

Vamos a considerar por ahora que el tipo de dato clock_t es


equivalente a tipo de dato long (de hecho así es). Esta función está
recomendada para medir intervalos de tiempo. El valor que devuelve es
proporcional al tiempo trascurrido desde el inicio de ejecución del
programa en la que se encuentra esa función. Ese valor devuelto será
mayor cuanto más tarde se ejecute esta función clock, que no realiza
tarea alguna más que devolver el valor actualizado del contador de
tiempo. Cada breve intervalo de tiempo (bastantes veces por segundo:
no vamos ahora a explicar este aspecto de la función) ese contador que
indica el intervalo de tiempo transcurrido desde el inicio de la ejecución
del programa, se incrementa en uno.

Un modo de estudiar el tiempo trascurrido en un proceso será el


siguiente:
time_t t1, t2;
t1 = clock();

312
Capítulo 11. Algunos usos con funciones.

(proceso a estudiar su tiempo)


t2 = clock();
printf(“Intervalo transcurrido: %ld”, t2 – t1);

El valor que imprimirá este código será proporcional al tiempo invertido


en la ejecución del proceso del que estudiamos su ejecución. Si esa
ejecución es muy rápida posiblemente el resultado sea cero.

Veamos ahora dos programas de ordenación. El primero mediante la


técnica de la burbuja. Como se ve, en esta ocasión trabajamos con un
vector de cien mil valores para ordenar:
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

#define TAM 100000


#define RANGO 10000

int cambiar(long*, long*);

void main(void)
{
long numeros[TAM];
long i, j;
time_t t1, t2;
randomize();
for(i = 0 ; i < TAM ; i++)
numeros[i] = random(RANGO);
// Vamos a ordenar esos numeros ...
// Método de la burbuja ...
t1 = clock();
for( i = 0 ; i < TAM ; i++)
for(j = i ; j < TAM ; j++)
if(numeros[i] > numeros[j])
cambiar(numeros + i, numeros + j);
t2 = clock();
printf("t2 - t1 = %ld.\n", t2 - t1);
}

int cambiar(long *a, long *b)


{
*a ^= *b;
*b ^= *a;
*a ^= *b;
}

313
Fundamentos de informática. Programación en Lenguaje C

Si lo ejecuta en su ordenador, le aparecerá por pantalla (quizá tarde


unos segundos: depende de lo rápido que sea su ordenador) una
número. Si es cero, porque la ordenación haya resultado muy rápida,
simplemente aumente el valor de TAM y vuelva a compilar y ejecutar el
programa.

Ahora escriba este otro programa, que ordena mediante la función


qsort. Es el que hemos visto antes, algo modificado para hacer la
comparación de tiempos:
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#define TAM 100000
#define RANGO 10000
int ordenar(void*,void*);

void main(void)
{
long numeros[TAM];
long i, j;
time_t t1, t2;
randomize();
for(i = 0 ; i < TAM ; i++)
numeros[i] = random(RANGO);

// Vamos a ordenar esos numeros ...


// Mediante la función qsort ...
t1 = clock();
qsort((void*)numeros, TAM, sizeof(long), ordenar);
t2 = clock();
printf("t2 - t1 = %ld.", t2 - t1);
}

int ordenar(void *a, void *b)


{
if(*(long*)a > * (long*)b)
return 1;
else if(*(long*)a < *(long*)b)
return -1;
return 0;
}

Si ejecuta ahora este programa, obtendrá igualmente la ordenación de


los elementos del vector. Pero ahora el valor que saldrá por pantalla es
del orden de 500 veces más bajo.

314
Capítulo 11. Algunos usos con funciones.

El algoritmo de ordenación de la burbuja es muy cómodo de


implementar, y es eficaz para la ordenación de unos pocos centenares
de enteros. Pero cuando hay que ordenar grandes cantidades, no es
suficiente con que el procedimiento sea teóricamente válido: además
debe ser eficiente.

Programar no es sólo poder en un lenguaje una serie de instrucciones.


Además de saber lenguajes de programación es conveniente conocer de
qué algoritmos se disponen para la solución de nuestros problemas. O
echar mano de soluciones ya adoptadas, como en este caso, la
implementación de la función qsort.

Creación de MACROS
La directiva #define, que ya hemos visto, permite la definición de
macros.

Una macro es un bloque de sentencias a las que se les ha asignado un


nombre o identificador.

Una macro no es una función. Es código que se inserta allí donde


aparece su identificador.

Veamos un ejemplo sencillo:


#include <stdio.h>
// Definición de la macro ...
#define cuadrado(x) x * x
void main(void)
{
short a;
unsigned long b;
printf("Intoduzca el valor de a ... ");
scanf("%hd",&a);
printf("El cuadrado de %hd es %lu", a, cuadrado(a));
}

cuadrado NO es una función, aunque su invocación tenga la misma


apariencia. En el código no aparece ni un prototipo con ese nombre, ni

315
Fundamentos de informática. Programación en Lenguaje C

su definición. Es una macro: el código “x * x” aparecerá allí donde en


nuestro programa se ponga cuadrado(x).

#define es una directiva del compilador. Antes de compilar, se busca en


todo el texto todas las veces donde venga escrita la cadena
“cuadrado(expresión)”. Y en todas ellas sustituye esa cadena por la
segunda parte de la directiva define: en este caso, lo sustituye por la
cadena “expresión * expresión”. En nuestro ejemplo hemos calculado el
cuadrado de la variable a; en general, se puede calcular el cuadrado de
cualquier expresión.

En definitiva una macro es un bloque de código que se va a insertar,


previamente a la compilación, en todas aquellas partes de nuestro
programa donde se encuentre su identificador.

Una macro puede hacer uso de otra macro. Por ejemplo:


#define cuadrado(x) x * x
#define circulo(r) 3.141596 * cuadrado(r)

La macro circulo calcula la superficie de una circunferencia de radio r.


Para realizar el cálculo, hace uso de la macro cuadrado, que calcula el
cuadrado del radio. La definición de una macro debe preceder siempre a
su uso. No se podría definir la macro circulo como se ha hecho si,
previamente a su definición, no estuviera recogida la definición de la
macro cuadrado.

Las macros pueden llegar a ser muy extensas. Vamos a rehacer el


código del programa del tema anterior sobre la criba de Erastóthenes,
usando macros en lugar de funciones.

Ejemplo de MACRO: la Criba de Erastóthenes


El código mediante funciones que resuelve la criba de Erastóthenes ha
quedado resuelto al final de un tema anterior. El propósito ahora es
rehacer toda la aplicación sin hacer uso de funciones: definiendo
macros.

316
Capítulo 11. Algunos usos con funciones.

Para poder ver la diferencia entre utilizar macros y utilizar funciones


convendrá presentar de nuevo las dos funciones que se habían definido
para la aplicación, y poder comparar cómo se rehacen mediante una
directiva de procesador.

Las dos funciones eran la función Criba y la función TablaPrimos. La


primera era:
long Criba(char* num, long rango)
{
long i, j;
// En principio marcamos todos los elementos como PRIMOS
for(i = 0 ; i < rango; i++)
num[i] = 'p';
for(i = 2 ; i < sqrt(rango) ; i++)
if(num[i] == 'p')
for(j = 2 * i ; j < rango ; j += i)
num[j] = 'c';
for( i = 1 , j = 0 ; i < rango ; i++)
if(num[i] == 'p') j++;
return j;
}

Ahora, con la macro, que hemos llamado __Criba, queda de la


siguiente forma:
#define __Criba(_num, _pr) \
{ \
long _i, _j; \
for(_i = 0 ; _i < MAX; _i++) _num[_i] = 'p'; \
for(_i = 2 ; _i < sqrt(MAX) ; _i++) \
if(_num[_i] == 'p') \
for(_j = 2 * _i ; _j < MAX ; _j += _i) \
_num[_j] = 'c'; \
for(_i = 1 , _j = 0 ; _i < MAX ; _i++) \
if(_num[_i] == 'p') _j++; \
_pr = _j; \
}

Las barras invertidas al final de cada línea indican que aunque hay un
salto de línea en el editor, el texto continúa en la línea siguiente. Deben
ponerse tal cual, sin espacio en blanco alguno posteriormente a ellas.

Las diferencias principales con respecto al código de la función se basan


en las siguientes peculiaridades de las macros:

317
Fundamentos de informática. Programación en Lenguaje C

1. El nombre: mucha gente habitúa a preceder al nombre de las macros


uno o varios caracteres subrayado; nosotros hemos utilizado dos. Es
cuestión de criterio personal, y es muy conveniente usar un criterio
de creación de identificadores especial para las macros. Si se decide
que las macros comienzan con dos caracteres subrayado, y tenemos
la disciplina de trabajo de no crear jamás, en código normal, un
identificador con ese inicio, entonces es imposible que la macro
pueda generar confusión. Dentro de las macros, es habitual también
darle un formato especial a los nombres de las variables que se
definan en ellas: en el ejemplo se han tomado todas las variables, y
todos los parámetros de la macro con un carácter subrayado al
principio. Es muy importante dar nombres especiales a esas
variables: hay que tener en cuenta que el código se inserta tal cual
en la función que invoca a la macro: en nuestro ejemplo, si las
variables de la macro se hubieran llamado i y j en lugar de _i y _j,
tendríamos un error de compilación, porque esas variables, con el
identificador i y con el identificador j ya están creadas en la función
principal que utiliza las macros.

2. Hay que considerar que la macro lo que hace es insertar el código en


el lugar donde se coloca el identificador: si necesitamos crear
variables, habrá que definir la macro como un bloque (comenzar y
terminar con llaves) para no tener que arrastrar luego todas esas
variables en el ámbito de la función que llama a la macro. Si la
función que convertimos en macro devolvía un valor, ahora habrá
que ver la manera de que ese valor quede recogido al final de la
macro: lo habitual será que se pase como parámetro la variable
donde iba a quedar almacenado el valor que devolvía la función. En
nuestro caso hemos pasado como parámetro la variable pr. La
variable que en la función se llamaba rango ha quedado eliminada
porque si trabajamos con macros podemos hacer uso de la que
define el valor de MAX, cosa que evitábamos al redactar la función:
no queríamos que la función tuviera un valor dependiente de una

318
Capítulo 11. Algunos usos con funciones.

macro definida en la aplicación donde se definía la función, para


permitir que la función fuese lo más transportable posible, y no
dependiente de un valor ajeno a su propia definición.

La segunda función era:


void TablaPrimos(char* num, long* primos, long rango)
{
long i, j;
for(i = 1 , j = 0 ; i < rango ; i++)
if(num[i] == 'p')
{
*(primos + j) = i;
j++;
}
*(primos + j) = 0;
}

Y ahora, con la macro, que hemos llamado __TablaPrimos, queda de


la siguiente forma:
#define __TablaPrimos(_num, _primos) \
{ \
long _i, _j; \
for(_i = 1 , _j = 0 ; _i < MAX ; _i++) \
if(_num[_i] == 'p') \
{ *(_primos + _j) = _i; \
_j++;} \
*(_primos + _j) = 0; \
}

Donde de nuevo hemos eliminado el uso del tercer parámetro que se


recogía en una variable llamada rango. Hemos mantenido el criterio
para la asignación de nombres. Hemos recibido como parámetros los
dos punteros: en la macro se llaman con una carácter subrayado previo:
cuando se sustituye el código de la macro en el programa, antes de la
compilación, el nombre que se recoge es el que se haya consignado
entre paréntesis en la llamada de la macro: y ahí los nombres van sin
esos caracteres subrayado.

Para un usuario que no haya definido la macro, el que un bloque de


código sea macro o sea función es algo que no ha de saber. El modo de
invocación es el mismo (cambiando los parámetros). Muchas de las

319
Fundamentos de informática. Programación en Lenguaje C

funciones estándares de C hacen uso de macros; otras no son realmente


tales, sino que son macros.

La ventaja de la macro es que el código queda insertado en la aplicación


antes de la compilación, de forma que su uso no exige la llamada a una
función, el apile en memoria de las variables que se deben guardar
mientras salimos de un ámbito para meternos en el ámbito de la nueva
función en ejecución, etc. El uso de macros reduce los tiempos de
ejecución de las aplicaciones notablemente. Una macro consume, de
media, un 20 % menos del tiempo total que tardaría, en hacer lo
mismo, el código definido en forma de función.

Funciones con un número variable de argumentos


Hasta el momento hemos visto funciones que tienen definido un número
de parámetros concreto. Y son, por tanto, funciones que al ser
invocadas se les debe pasar un número concreto y determinado de
parámetros.

Sin embargo no todas las funciones que hemos utilizado son realmente
así de rígidas. Por ejemplo, la función printf, tantas veces invocada en
nuestros programas, no tiene un número prefijado de parámetros:

printf(“Aquí solo hay un parametro.”); // Un parámetro

printf(“Aquí hay %ld parámetros.”, 2); // Dos parámetros

printf(“Y ahora %ld%c”, 3, ‘.’); // Tres parámetros

Vamos a ver en este epígrafe cómo lograr definir una función en la que
el número de parámetros sea variable, en función de las necesidades
que tenga el usuario en cada momento.

Existen una serie de macros que permiten definir una función como si
tuviera una lista variable de parámetros. Esas macros, que ahora
veremos, están definidas en la biblioteca stdarg.h.

320
Capítulo 11. Algunos usos con funciones.

El prototipo de las funciones con un número variable de parámetros es


el siguiente:

tipo nombre_funcion(tipo_1,[..., tipo_N], ...);

Primero se recogen todos los parámetros de la función que son fijos, es


decir, aquellos que siempre deberán aparecer como parámetros en la
llamada a la función. En el caso de la función printf, siempre debe
aparecer, al principio, una cadena de caracteres, que viene recogida
entre comillas. Si falta esta cadena en la función printf tendremos error
en tiempo de compilación.

Y después de los parámetros fijos y obligatorios (como veremos más


adelante, toda función que admita un número variable de parámetros, al
menos deberá tener un parámetro fijo) vienen tres puntos suspensivos.
Esos puntos deben ir al final de la lista de argumentos conocidos, e
indican que la función puede tener más argumentos, de los que no
sabemos ni cuántos ni de qué tipo de dato.

La función que tiene un número indeterminado de parámetros, deberá


averiguar cuáles recibe en cada llamada. La lista de parámetros deberá
ser recogida por la función, que deberá deducir de esa lista cuáles son
los parámetros recibidos. Para almacenar y operar con esta lista de
argumentos, está definido, en la biblioteca stdarg.h, un nuevo tipo de
dato de C, llamado va_list (podríamos decir que es el tipo de “lista de
argumentos”). En esa biblioteca viene definido el tipo de dato y las tres
macros empleadas para operar sobre objetos de tipo lista de
argumentos. Este tipo de dato tendrá una forma similar a una cadena de
caracteres.

Toda función con un número de argumentos variable deberá tener


declarada, en su cuerpo, una variable de tipo de dato va_list.
tipo nombre_funcion (tipo_1,[..., tipo_N], ...)
{
va_list argumentos /* lista de argumentos */

321
Fundamentos de informática. Programación en Lenguaje C

Lo primero que habrá que hacer con esta variable de tipo va_list será
inicializarla con la lista de argumentos variables recibida en la llamada a
la función.

Para inicializar esa variable se emplea una de las tres macros definidas
en la biblioteca stdarg.h: la macro va_start, que tiene la siguiente
sintaxis:

void va_start(va_list ap, lastfix);

Donde ap es la variable que hemos creado como de tipo de dato


va_list, y donde lastfix es el último argumento fijo de la lista de
argumentos.

La macro va_start asigna a la variable ap la dirección del primer


argumento variable que ha recibido la función. Necesita, para esta
operación, recibir el nombre del último parámetro fijo que recibe la
función como argumento. Se guarda en la variable ap la dirección del
comienzo de la lista de argumentos variables. Esto obliga a que en
cualquier función con número de argumentos variable exista al menos
un argumento de los llamados aquí fijos, con nombre en la definición de
la función. En caso contrario, sería imposible obtener la dirección del
comienzo para una lista de los restantes argumentos.

Ya tenemos localizada la cadena de argumentos variables. Ahora será


necesario recorrerla para extraer todos los argumentos que ha recibido
la función en su actual llamada. Para eso está definida una segunda
macro de stdarg.h: la macro va_arg. Esta rutina o macro extrae el
siguiente argumento de la lista.

Su sintaxis es:

tipo va_arg(va_list ap, tipo);

Donde el primer argumento es, de nuevo, nuestra lista de argumentos,


llamada ap, que ya ha quedado inicializada con la macro va_start. Y
tipo es el tipo de dato del próximo parámetro que se espera encontrar.

322
Capítulo 11. Algunos usos con funciones.

Esa información es necesaria: por eso, en la función printf, indicamos en


el primer parámetro (la cadena que ha de ser impresa) los
especificadores de formato.

La rutina va extrayendo uno tras otro los argumentos de la lista variable


de argumentos. Para cada nuevo argumento se invoca de nuevo a la
macro. La macro extrae de la lista ap el siguiente parámetro (que será
del tipo indicado) y avanza el puntero al siguiente parámetro de la lista.
La macro devuelve el valor extraído de la lista. Para extraer todos los
elementos de la lista habrá que invocar a la macro va_arg tantas veces
como sea necesario. De alguna manera la función deberá detectar que
ha terminado ya de leer en la lista de variables. Por ejemplo, en la
función printf, se invocará a la macro va_arg tantas veces como veces
haya aparecido en la primera cadena un especificador de formato: un
carácter % no precedido del carácter ‘\’.

Si se ejecuta la macro va_arg menos veces que parámetros se hayan


pasado en la actual invocación, la ejecución no sufre error alguno:
simplemente dejarán de leerse esos argumentos. Si se ejecuta más
veces que parámetros variables se hayan pasado, entones el resultado
puede ser imprevisible.

Si, después de la cadena de texto que se desea imprimir, la función


printf recoge más expresiones (argumentos en la llamada) que
caracteres ‘%’ ha consignado en la cadena (primer argumento de la
llamada), no pasará percance alguno: simplemente habrá argumentos
que no se imprimirán y ni tan siquiera serán extraídos de la lista de
parámetros. Pero si hay más caracteres ‘%’ que variables e nuestra lista
variable de argumentos, entonces la función printf ejecutará la macro
va_arg en busca de argumentos no existentes. En ese caso, el
resultado será completamente imprevisible.

Y cuando ya se haya recorrido completa la lista de argumentos,


entonces deberemos ejecutar una tercera rutina que restablece la pila
de llamada a funciones. Esta macro es necesaria para permitir la

323
Fundamentos de informática. Programación en Lenguaje C

finalización correcta de la función y que pueda volver el control de


programa a la sentencia inmediatamente posterior a la de la llamada de
la función de argumentos variables.

Su sintaxis es:

void va_end(va_list ap);

Veamos un ejemplo. Hagamos un programa que calcule la suma de una


serie de variables double que se reciben. El primer parámetro de la
función indicará cuántos valores intervienen en la suma; los demás
parámetros serán esos valores. La función devolverá la suma de todos
ellos:
#include <stdio.h>
#include <stdarg.h>

double sum(long, ...);

void main(void)
{
double S;
S = sum(7, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0);
printf("%f",S);
}

double sum(long v,...)


{
double suma = 0;
long i;
va_list sumandos;
va_start(sumandos, v);
for(i = 0 ; i < v ; i++)
suma += va_arg(sumandos,double);
va_end(sumandos);
return suma;
}

La función sumaf recibe un único parámetro fijo, que es el que indica


cuántas variables más va a recibir la función. Así cuando alguien quiere
sumar varios valores, lo que hace es invocar a la función sumaf
indicándole en un primer parámetro el número de sumandos y, a
continuación, esos sumandos que se ha indicado.

324
Capítulo 11. Algunos usos con funciones.

Después de inicializar la variable sumandos de tipo va_list mediante la


macro va_start, se van sumando todos los argumentos recibidos en la
variable suma. Cada nuevo sumando se obtiene de la cadena sumandos
gracias a una nueva invocación de la macro va_arg. Tendré tantas
sumas como indique el parámetro fijo recibido en la variable v.

Al final, y antes de la sentencia return, ejecutamos la macro que


restaura la pila de direcciones de memoria (va_end), de forma que al
finalizar la ejecución de la función el programa logrará transferir el
control a la siguiente sentencia posterior a la que invocó la función de
parámetros variables.

Observación: las funciones con parámetros variables presentan


dificultades cuando deben cargar, mediante la macro va_arg, valores
de tipo char, unsigned char y valores float. Hay problemas de
promoción de variables y los resultados no son finalmente los
esperados.

Argumentos de la línea de órdenes


Ya hemos explicado que en todo programa, la única función ejecutable
es la función principal: la función main. Un programa sin función
principal puede ser compilable, pero no llegará a generar un programa
ejecutable, porque no tiene la función de arranque.

Así, vemos que todas las funciones de C pueden definirse con


parámetros: es decir, pueden ejecutarse con unos valores de arranque,
que serán diferentes cada vez que esa función sea invocada.

También se puede hacer eso con la función main. En ese caso, quien
debe pasar los parámetros de arranque a la función principal será el
usuario del programa compilado en el momento en que indique al
sistema operativo el inicio de esa ejecución de programa.

325
Fundamentos de informática. Programación en Lenguaje C

En muchos sistemas operativos (UNIX especialmente) es posible,


cuando se ejecuta un programa compilado de C, pasar parámetros a la
función main. Esos parámetros se pasan en la línea de comandos que
lanza la ejecución del programa. Para ello, en esa función principal se
debe haber incluido los siguientes parámetros:

tipo main(int argc, char *argv[] )

Donde argc recibe el número de argumentos de la línea de comandos,


y argv es un array de cadenas de caracteres donde se almacenan los
argumentos de la línea de comandos.

Los usos más comunes para los argumentos pasados a la función


principal son el pasar valores para la impresión, el paso de opciones de
programa (muy empleado eso en unix, o en DOS), el paso de nombres
de archivos donde acceder a información en disco o donde guardar la
información generada por el programa, etc.

La función main habrá recibido tantos argumentos como diga la variable


argc. El primero de esos argumentos es siempre el nombre del
programa. Los demás argumentos deben ser valores esperados, de
forma que la función principal sepa qué hacer con cada uno de ellos.
Alguno de esos argumentos puede ser una cadena de control que
indique la naturaleza de los demás parámetros, o al menos de alguno de
ellos.

Desde luego, los nombres de las variables argc y argv son mera
convención: cualquier identificador que se elija servirá de la misma
manera.

Y un último comentario. Hasta el momento, siempre que hemos definido


la función main, la hemos declarado de tipo void. Realmente esta
función puede ser de cualquier tipo, y en tal caso podría disponer de una
sentencia return que devolviese un valor de ese tipo de dato.

Veamos un ejemplo. Hacemos un programa que al ser invocado se le


pueda facilitar una serie de datos personales, y los muestre por pantalla.

326
Capítulo 11. Algunos usos con funciones.

El programa espera del usuario que introduzca su nombre, su profesión


y/o su edad. Si el usuario quiere introducir el nombre, debe precederlo
con la cadena “-n”; si quiere introducir la edad, deberá precederla la
cadena “-e”; y si quiere introducir la profesión, deberá ir precedida de la
cadena “-p”.
#include <stdio.h>
#include <string.h>
void main(int argc, char*argv[])
{
char nombre[30];
char edad[5];
char profesion[30];
nombre[0] = profesion[0] = edad[0] = '\0';
long i;
for(i = 0 ; i < argc ; i++)
{
if(strcmp(argv[i],"-n") == 0)
{
i++;
if(i < argc) strcpy(nombre, argv[i]);
}
else if(strcmp(argv[i],"-p") == 0)
{
i++;
if(i < argc) strcpy(profesion, argv[i]);
}
else if(strcmp(argv[i],"-e") == 0)
{
i++;
if(i < argc) strcpy(edad, argv[i]);
}
}
printf("Nombre: %s\n", nombre);
printf("Edad: %s\n", edad);
printf("Profesion: %s\n", profesion);
}

Si ha entrado la cadena “–n”, entonces la siguiente cadena deberá ser el


nombre: y así se almacena. Si se ha entrado la cadena “–p”, entonces la
siguiente cadena será la profesión: y así se guarda. Y lo mismo ocurre
con la cadena “–e” y la edad.

Al compilar el programa, quedará el ejecutable en algún directorio: en


principio en el mismo en donde se haya guardado el documento .cpp. Si

327
Fundamentos de informática. Programación en Lenguaje C

ejecutamos ese programa (supongamos que le hemos llamado “datos”)


con la siguiente línea de comando:

datos –p estudiante –e 21 –n Isabel

Aparecerá por pantalla la siguiente información:


Nombre: Isabel
Edad: 21
Profesion: estudiante

Ejercicios

66. Una vez ha creado un vector que contiene todos los primos
menores que un límite superior dado, declare y defina una
función que busque los primos enlazados. Se llaman primos
enlazados a aquellos primos cuya diferencia es igual a dos, es
decir, que son dos impares consecutivos: por ejemplo 11 t 13;
17 y 19, etc. (Sin resolver)

67. Lea el siguiente código y muestre la salida que ofrecerá por


pantalla.

#include <stdio.h>
#include <math.h>
#define CUADRADO(x) x*x
#define SUMA(x,y) CUADRADO(x) + CUADRADO(y)
#define RECTO(x,y) sqrt(SUMA(x,y))

void main(void)
{
printf("%2.1f",RECTO(3,4));
}

328
CAPÍTULO 12
ESTRUCTURAS ESTÁTICAS DE
DATOS Y DEFINICIÓN DE TIPOS

En uno de los primeros temas hablamos largamente de los tipos de


dato. Decíamos que un tipo de dato determina un dominio (conjunto de
valores posibles) y unos operadores definidos sobre esos valores.

Hasta el momento hemos trabajado con tipos de dato estándar en C.


Pero con frecuencia hemos hecho referencia a que se pueden crear otros
diversos tipos de dato, más acordes con las necesidades reales de
muchos problemas concretos que se abordan con la informática.

Hemos visto, de hecho, ya diferentes tipos de dato a los que por ahora
no hemos prestado atención alguna, pues aún no habíamos llegado a
este capítulo: tipo de dato size_t, ó time_t: cada vez que nos los
hemos encontrado hemos despejado con la sugerencia de que se
considerasen, sin más, tipos de dato iguales a long.

En este tema vamos a ver cómo se pueden definir nuevos tipos de dato.
Fundamentos de informática. Programación en Lenguaje C

Tipos de dato enumerados


La enumeración es el modo más simple de crear un nuevo tipo de dato.
Cuando definimos un tipo de dato enumerado lo que hacemos es definir
de forma explícita cada uno de los valores que formarán parte del
dominio de ese nuevo tipo de dato: es una definición por extensión.

La sintaxis de creación de un nuevo tipo de dato enumerado es la


siguiente:

enum identificador {id_1[, id_2, ..., id_N]};

Donde enum es una de las 32 palabras reservadas de C. Donde


identificador es el nombre que va a recibir el nuevo tipo de dato. Y
donde id_1, etc. son los diferentes identificadores de cada uno de los
valores del nuevo dominio creado con el nuevo tipo de dato.

Mediante la palabra clave enum se logran crear tipos de dato que son
subconjunto de los tipos de dato int. Los tipos de dato enumerados
tienen como dominio un subconjunto del dominio de int. De hecho las
variables creadas de tipo enum son tratadas, en todo momento, como
si fuesen de tipo int. Lo que hace enum es mejorar la legibilidad del
programa. Pero a la hora de operar con sus valores, se emplean todos
los operadores definidos para int.

Veamos un ejemplo:

enum semaforo {verde, amarillo, rojo};

Al crear un tipo de dato así, acabamos de definir:

1. Un dominio: tres valores definidos, con los literales “verde”,


“amarillo” y “rojo”. En realidad el ordenador los considera valores 0,
1 y 2.

2. Una ordenación intrínseca a los valores del nuevo dominio: verde


menor que amarillo, y amarillo menor que rojo.

330
Capítulo 12. Estructuras estáticas de datos y definición de tipos.

Acabamos, pues, de definir un conjunto de valores (subconjunto de los


enteros), con identificadores propios y únicos, que presenta la propiedad
de ordenación.

Luego, en la función que sea necesario utilizar ese tipo de dato, se


pueden declarar variables con la siguiente sintaxis:

enum identificador nombre_variable;

En el caso del tipo de dato semáforo, podemos definir en una función


una variable de la siguiente forma:

enum semaforo cruce;

Veamos otro ejemplo


enum judo {blanco,amarillo,naranja,verde,azul,marron,negro};

void main(void)
{
enum judo c;
printf(“Los colores definidos son ... \n”);
for(c = blanco ; c <= negro ; c++)
printf("%d\t",c);
}

La función principal mostrará por pantalla los valores de todos los


colores definidos: el menor es el banco, y el mayor el negro. Por
pantalla aparecerá la siguiente salida:
Los colores definidos son ...
0 1 2 3 4 5 6

Dar nombre a los tipos de dato


A lo largo del presente capítulo veremos la forma de crear diferentes
estructuras de datos que den lugar a tipos de dato nuevos. Luego, a
estos tops de dato se les puede asignar un identificador o un nombre
para poder hacer referencia a ellos a la hora de crear nuevas variables.

La palabra clave typedef, de C, permite crear nuevos nombres para los


tipos de dato creados. Una vez se ha creado un tipo de dato y se ha

331
Fundamentos de informática. Programación en Lenguaje C

creado el nombre para hacer referencia a él, ya podemos usar ese


identificador en la declaración de variables, como si fuese un tipo de
dato estándar en C.

En sentido estricto, las sentencias typedef no crean nuevos tipos de


dato: lo que hacen es asignar un identificador definitivo para esos tipos
de dato.

La sintaxis para esa creación de identificadores es la siguiente:

typedef tipo nombre_tipo;

Así, se pueden definir los tipos de dato estándar con otros nombres, que
quizá convengan por la ubicación en la que se va a hacer uso de esos
valores definidos por el tipo de dato. Y así tenemos:
typedef unsinged size_t;
typedef long time_t;

O podemos nosotros mismos reducir letras:


typedef unsigned long int uli;
typedef unsigned short int usi;
typedef signed short int ssi;
typedef signed long int sli;

O también:

typedef char* CADENA;

Y así, a partir de ahora, en todo nuestro programa, nos bastará declarar


las variables enteras como uno de esos nuevos cuatro tipos. O declarar
una cadena de caracteres como una variable de tipo CADENA. Es
evidente que con eso no se ha creado un nuevo tipo de dato, sino
simplemente un nuevo identificador para un tipo de dato ya existente.

También se puede dar nombre a los nuevos tipos de dato creados. En el


ejemplo del tipo de dato enum llamado semaforo, podríamos hacer:

typedef enum {verde, amarillo, rojo} semaforo;

332
Capítulo 12. Estructuras estáticas de datos y definición de tipos.

Y así, el identificador semáforo quedaría definitivamente como nuevo


tipo de dato. Luego, en una función que necesitase un tipo de dato de
este tipo, ya no diríamos:

enum semaforo cruce;

Sino simplemente

semaforo cruce;

Ya que el identificador semaforo ya ha quedado como identificador


válido de tipo de dato en C.

Estructuras de datos y tipos de dato estructurados


Comenzamos ahora a tratar de la creación de verdaderos nuevos tipos
de dato. En C, además de los tipos de dato primitivos, se pueden utilizar
otros tipos de dato definidos por el usuario. Son tipos de dato que
llamamos estructurados, que se construyen mediante componentes de
tipos más simples previamente definidos o tipos de dato primitivos, que
se denominan elementos de tipo constituyente. Las propiedades que
definen un tipo de dato estructurado son el número de componentes que
lo forman (que llamaremos cardinalidad), el tipo de dato de los
componentes y el modo de referenciar a cada uno de ellos.

Un ejemplo de tipo de dato estructurado ya lo hemos definido y utilizado


de hecho: las matrices y los vectores. No hemos considerado esas
construcciones como una creación de un nuevo tipo de dato sino como
una colección ordenada y homogénea de una cantidad fija de elementos,
todos ellos del mismo tipo, y referenciados uno a uno mediante índices.

Pero existe otro modo, en C, de crear un tipo de dato estructurado. Y a


ese nos queremos referir cuando decimos que creamos un nuevo tipo de
dato, y no solamente una colección ordenada de elementos del mismo
tipo. Ese tipo de dato se llama registro, y está formado por
yuxtaposición de elementos que contienen información relativa a una

333
Fundamentos de informática. Programación en Lenguaje C

misma entidad. Por ejemplo, el tipo de dato asignatura puede tener


diferentes elementos, todos ellos relativos a la entidad asignatura, y no
todos ellos del mismo tipo. Y así, ese tipo de dato registro que hemos
llamado asignatura tendría un elemento que llamaríamos clave y que
podría ser de tipo long; y otro campo se llamaría descripción y sería de
tipo char*; y un tercer elemento sería el número de créditos y sería de
tipo float, etc. A cada elemento de un registro se le llama campo.

Un registro es un tipo de dato estructurado heterogéneo, donde no


todos los elementos (campos) son del mismo tipo. El dominio de este
tipo de dato está formado por el producto cartesiano de los diferentes
dominios de cada uno de los componentes. Y el modo de referenciar a
cada campo dentro del registro es mediante el nombre que se le dé a
cada campo.

En C, se dispone de una palabra reservada para la creación de registros:


la palabra struct.

Estructuras de datos en C
Una estructura de datos en C es una colección de variables, no
necesariamente del mismo tipo, que se referencian con un nombre
común. Lo normal será crear estructuras formadas por variables que
tengan alguna relación entre sí, de forma que se logra compactar la
información, agrupándola de forma cabal. Cada variable de la estructura
se llama, en el lenguaje C, elementos de la estructura. Este concepto es
equivalente al presentado antes al hablar de campos.

La sintaxis para la creación de estructuras presenta diversas formas.


Empecemos viendo una de ellas:
struct nombre_estructura
{
tipo_1 identificador_1;
tipo_2 identificador_2;
...
tipo_N identificador_N;

334
Capítulo 12. Estructuras estáticas de datos y definición de tipos.

};

La definición de la estructura termina, como toda sentencia de C, en un


punto y coma.

Una vez se ha creado la estructura, y al igual que hacíamos con las


uniones, podemos declarar variables del nuevo tipo de dato dentro de
cualquier función del programa donde está definida la estructura:

struct nombre_estructura variable_estructura;

Y el modo en que accedemos a cada uno de los elementos (o campos)


de la estructura (o registro) será mediante el operador miembro, que
se escribe con el identificador punto (.):

variable_estructura.identificador_1

Y, por ejemplo, para introducir datos en la estructura haremos:

variable_estructura.identificador_1 = valor_1;

La declaración de una estructura se hace habitualmente fuera de


cualquier función, puesto que el tipo de dato trasciende el ámbito de
una función concreta. De todas formas, también se puede crear el tipo
de dato dentro de la función, cuando ese tipo de dato no va a ser
empleado más allá de esa función. En ese caso, quizá no sea necesario
siquiera dar un nombre a la estructura, y se pueden crear directamente
las variables que deseemos de ese tipo, con la siguiente sintaxis:
struct
{
tipo_1 identificador_1;
tipo_2 identificador_2;
...
tipo_N identificador_N;
}nombre_variable;

Y así queda definida la variable nombre_variable, de tipo de dato


struct. Evidentemente, esta declaración puede hacerse fuera de
cualquier función, de forma que la variable que generemos sea de
ámbito global.

335
Fundamentos de informática. Programación en Lenguaje C

Otro modo de generar la estructura y a la vez declarar las primeras


variables de ese tipo, será la siguiente sintaxis:
struct nombre_estructura
{
tipo_1 identificador_1;
tipo_2 identificador_2;
...
tipo_N identificador_N;
}variable_1, ..., variable_N;

Y así queda definido el identificador nombre_estructura y quedan


declaradas las variables variable_1, …, variable_N, que serán locales o
globales según se hay hecho esta declaración en uno u otro ámbito.

Lo que está claro es que si la declaración de la estructura se realiza


dentro de una función, entonces únicamente dentro de su ámbito el
identificador de la estructura tendrá el significado de tipo de dato, y
solamente dentro de esa función se podrán utilizar variables de ese tipo
estructurado.

El método más cómodo para la creación de estructuras en C es


mediante la combinación de la palabra struct de la palabra typedef. La
sintaxis de esa forma de creación es la siguiente:
typedef struct
{
tipo_1 identificador_1;
tipo_2 identificador_2;
...
tipo_N identificador_N;
} nombre_estructura;

Y así, a partir de este momento, en cualquier lugar del ámbito de esta


definición del nuevo tipo de dato, podremos crear variables con la
siguiente sintaxis:

nombre_estructura nombre_variable;

Veamos algún ejemplo: podemos necesitar definir un tipo de dato que


podamos luego emplear para realizar operaciones en variable compleja.

336
Capítulo 12. Estructuras estáticas de datos y definición de tipos.

Esta estructura, que podríamos llamar complejo, tendría la siguiente


forma:
typedef struct
{
double real;
double imag;
}complejo;

Y también podríamos definir una serie de operaciones, mediante


funciones: por ejemplo, la suma, la resta y el producto de complejos. El
programa completo podría tener la siguiente forma:
#include <stdio.h>
typedef struct
{
double real;
double imag;
}complejo;

complejo sumac(complejo, complejo);


complejo restc(complejo, complejo);
complejo prodc(complejo, complejo);
void mostrar(complejo);

void main (void)


{
complejo A, B, C;
printf("Introducción de datos ... \n");
printf("Parte real de A ......... ");
scanf("%lf",&A.real);
printf("Parte imaginaria de A ... ");
scanf("%lf",&A.imag);
printf("Parte real de B ......... ");
scanf("%lf",&B.real);
printf("Parte imaginaria de B ... ");
scanf("%lf",&B.imag);
// SUMA ...
printf("\n\n");
mostrar(A);
printf(" + ");
mostrar(B);
C = sumac(A,B);
mostrar(C);

// RESTA ...
printf("\n\n");
mostrar(A);
printf(" - ");
mostrar(B);

337
Fundamentos de informática. Programación en Lenguaje C

C = restc(A,B);
mostrar(C);

// PRODUCTO ...
printf("\n\n");
mostrar(A);
printf(" * ");
mostrar(B);
C = prodc(A,B);
mostrar(C);
}

complejo sumac(complejo c1, complejo c2)


{
c1.real += c2.real;
c1.imag += c2.imag;
return c1;
}

complejo restc(complejo c1, complejo c2)


{
c1.real -= c2.real;
c1.imag -= c2.imag;
return c1;
}

complejo prodc(complejo c1, complejo c2)


{
complejo S;
S.real = c1.real + c2.real; - c1.imag * c2.imag;
S.imag = c1.real + c2.imag + c1.imag * c2.real;
return S;

void mostrar(complejo X)
{
printf("(% .2lf%s%.2lf * i) ", X.real, X.imag > 0 ? "
+" : " " , X.imag);
}

Así podemos ir definiendo un nuevo tipo de dato, con un dominio que es


el producto cartesiano del dominio de los double consigo mismo, y con
unos operadores definidos mediante funciones.

Las únicas operaciones que se pueden hacer sobre la estructura (aparte


de las que podamos definir mediante funciones) son las siguientes:
operador dirección (&), porque toda variable, también las estructuradas,

338
Capítulo 12. Estructuras estáticas de datos y definición de tipos.

tienen una dirección en la memoria; operador selección (.) mediante el


cual podemos acceder a cada uno de los elementos de la estructura; y
operador asignación, que sólo puede de forma que los dos extremos de
la asignación (tanto el Lvalue como el Rvalue) sean variables objeto del
mismo tipo. Por ejemplo, se puede hacer la asignación:
complejo A, B;
A.real = 2;
A.imag = 3;
B = A;

Y así, las dos variables valen lo mismo: al elemento real de B se le


asigna el valor consignado en la parte real de A; y al elemento imag de
B se le asigna el valor del elemento imag de A.

Otro ejemplo de estructura podría ser el que antes hemos iniciado, al


hablar de los registros: una estructura para definir un tipo de dato que
sirva para el manejo de asignaturas:
typedef struct
{
long clave;
char descripcion[50];
float creditos;
}asignatura;

Vectores y punteros a estructuras


Una vez hemos creado un nuevo tipo de dato estructurado, no resulta
extraño que podamos crear vectores y matrices de este nuevo tipo de
dato.

Si, por ejemplo, deseamos hacer un inventario de asignaturas, será


lógico que creemos un array de tantas variables asignatura como sea
necesario.

asignatura curricula[100];

Y así, tenemos 100 variables del tipo asignatura, distribuidas en la


memoria de forma secuencial, una después de la otra. El modo en que

339
Fundamentos de informática. Programación en Lenguaje C

accederemos a cada una de las variables será, como siempre mediante


la operatoria de índices: curricula[i]. Y si queremos acceder a algún
miembro de una variable del tipo estructurado, utilizaremos de nuevo el
operador de miembro: curricula[i].descripción.

También podemos trabajar con operatoria de punteros. Así como antes


hemos hablado de curricula[i], también podemos llegar a esa variable
del array con la expresión *(curricula + i). De nuevo, todo es igual.

Donde hay un cambio es en el operador de miembro: si trabajamos con


operatoria de punteros, el operador de miembro ya no es el punto,
sino que está formado por los caracteres “->”. Si queremos hacer
referencia al elemento o campo descripcion de una variable del tipo
asignatura, la sintaxis será: *(curricula + i)->descripcion.

Y también podemos trabajar con asignación dinámica de memoria. En


ese caso, se declara un puntero del tipo estructurado, y luego se le
asigna la memoria reservada mediante la función malloc. Si creamos un
array de asignaturas en memoria dinámica, un programa de gestión de
esas asignaturas podría ser el siguiente:
#include <stdio.h>
#include <stdlib.h>
typedef struct
{
long clave;
char descr[50];
float cred;
}asig;

void main(void)
{
asig *curr;
short n, i;
printf("Indique nº de asignaturas de su CV ... ");
scanf("%hd",&n);
/* La variable n recoge el número de elementos de tipo
asignatura que debe tener nuestro array. */
curr = (asig*)malloc(n * sizeof(asig));
if(curr == NULL)
{
printf("Memoria insuficiente.\n");
printf("La ejecucion se interrumpira.\n");

340
Capítulo 12. Estructuras estáticas de datos y definición de tipos.

printf("Pulse una tecla para terminar ... ");


getchar();
exit(0);
}
for(i = 0 ; i < n ; i++)
{
printf("\n\nAsignatura %hd ... \n",i + 1);
printf("clave ......... ");
scanf("%ld",&(curr + i)->clave);
printf("Descripcion ... ");
gets((curr + i)->descr);
printf("creditos ...... ");
scanf("%f",&(curr + i)->cred);
}
// Listado ...
for(i = 0 ; i < n ; i++)
{
printf("(%10ld)\t",(curr + i)->clave);
printf("%s\t",(curr + i)->descr);
printf("%4.1f creditos\n",(curr + i)->cred);
}
}

Observamos que (curr + i) es la dirección de la posición i-ésima del


vector curr. Es, pues, una dirección. Y (curr + i)->clave es el valor del
campo clave de la variable que está en la posición i-ésima del vector
curr. Es, pues, un valor: no es una dirección. Y (curr + i)->descr es la
dirección de la cadena de caracteres que forma el campo descr de la
variable que está en la posición i-ésima del vector curr. Es, pues, una
dirección, porque dirección es el campo descr: un array de caracteres.

Que accedamos a la variable estructura a través de un puntero o a


través de su identificador influye únicamente en el operador de miembro
que vayamos a utilizar. Una vez tenemos referenciado a través de la
estructura un campo o miembro concreto, éste será tratado como
dirección o como valor dependiendo de que el miembro se haya
declarado como puntero o como variable de dato.

Anidamiento de estructuras
Podemos definir una estructura que tenga entre sus miembros una
variable que sea también de tipo estructura. Por ejemplo:

341
Fundamentos de informática. Programación en Lenguaje C

typedef struct
{
unsigned short dia;
unsigned short mes;
unsigned short anyo;
}fecha;

typedef struct
{
unsigned long clave;
char descripcion[50];
double creditos;
fecha convocatorias[3];
}asignatura;

Ahora a la estructura de datos asignatura le hemos añadido un vector de


tres elementos para que pueda consignar sus fechas de exámenes en
las tres convocatorias. EL ANSI C permite hasta 15 niveles de
anidamiento de estructuras.

El modo de llegar a cada campo de la estructura fecha es, como


siempre, mediante los operadores de miembro. Por ejemplo, si
queremos que la primera convocatoria se realice el 15 de enero del
presente año, la segunda convocatoria el 21 de junio y la tercera el 1 de
septiembre, las órdenes deberán ser:
#include <time.h>
#include <stdio.h>
#include <string.h>

typedef struct
{
unsigned short dia;
unsigned short mes;
unsigned short anyo;
}fecha;

typedef struct
{
unsigned long clave;
char descripcion[50];
double creditos;
fecha c[3];
}asignatura;

void main(void)
{

342
Capítulo 12. Estructuras estáticas de datos y definición de tipos.

asignatura asig;
time_t bloquehoy;
struct tm *hoy;
bloquehoy = time(NULL);
hoy = localtime(&bloquehoy);

asig.clave = 10102301;
*asig.descripcion = '\0';
strcat(asig.descripcion,"fundamentos de informática");
asig.creditos = 7.5;

asig.c[0].dia = 15;
asig.c[0].mes = 1;
asig.c[0].anyo = hoy->tm_year - 100;

asig.c[1].dia = 21;
asig.c[1].mes = 6;
asig.c[1].anyo = hoy->tm_year - 100;

asig.c[2].dia = 1;
asig.c[2].mes = 9;
asig.c[2].anyo = hoy->tm_year - 100;

printf("Asignatura %10ld\n",asig.clave);
printf("%s\t",asig.descripcion);
printf("%4.1lf\n", asig.creditos);
printf("\npriemra convocatoria ... ");
printf("%2hu-%2hu-%02hu", asig.c[0].dia,
asig.c[0].mes,asig.c[0].anyo);
printf("\nsegunda convocatoria ... ");
printf("%2hu-%2hu-%02hu", asig.c[1].dia,
asig.c[1].mes,asig.c[1].anyo);
printf("\ntercera convocatoria ... ");
printf("%2hu-%2hu-%02hu", asig.c[2].dia,
asig.c[2].mes,asig.c[2].anyo);
}

Hemos asignado a cada uno de los tres elementos del vector c los
valores de día, mes y año correspondientes a cada una de las tres
convocatorias. Hemos utilizado índices de vectores para referenciar cada
una de las tres fechas. Podríamos haber trabajado también con
operatoria de punteros. Por ejemplo, la referencia al día de la segunda
convocatoria es, con operatoria de índices

asig.c[1].dia = 21;

y mediante operatoria de punteros:

343
Fundamentos de informática. Programación en Lenguaje C

(asig.c + 1)->dia = 21;

Donde asig.c es la dirección del primer elemento del vector c.

Y donde (asig.c + 1) es la dirección del segundo elemento del vector c.

Y donde (asig.c + 1)->dia es el valor del campo día de la variable de


tipo fecha cuya dirección es (asig.c + 1).

Respecto a la función localtime, el tipo de dato estructurado tm, y sus


campos (entre ellos el campo tm_year) puede encontrarse abundante
información sobre todo ello en la ayuda on line que ofrece cualquier
compilador. Son definiciones que vienen recogidas en la biblioteca
time.h y son estándares de ANSI C.

Tipo de dato union


Además de las estructuras, el lenguaje C permite otra forma de creación
de un nuevo tipo de dato: mediante la creación de una unión (que se
define mediante la palabra clave en C union: por cierto, con ésta,
acabamos de hacer referencia en este manual a la última de las 32
palabras del léxico del lenguaje C).

Una unión es una posición de memoria compartida por dos o más


variables diferentes, y en general de distinto tipo. Es una región de
memoria que, a lo largo del tiempo, puede contener objetos de diversos
tipos. Una unión permite almacenar tipos de dato diferentes en el mismo
espacio de memoria. Como las estructuras, las uniones también tienen
miembros; pero a diferencia de las estructuras, donde la memoria que
ocupan es igual a la suma del tamaño de cada uno de sus campos, la
memoria que emplea una variable de tipo unión es la necesaria para el
miembro de mayor tamaño dentro de la unión. La unión almacena
únicamente uno de los valores definidos en sus miembros.

La sintaxis para la creación de una unión es muy semejante a la


empleada para la creación de una estructura:

344
Capítulo 12. Estructuras estáticas de datos y definición de tipos.

typedef union
{
tipo_1 identificador_1;
tipo_2 identificador_2;
...
tipo_N identificador_N;
} nombre_union;

O en cualquiera otra de las formas que hemos visto para la creación de


estructuras.

Es responsabilidad del programador mantener la coherencia en el uso de


esta variable: si la última vez que se asignó un valor a la unión fue
sobre un miembro de un determinado tipo, luego, al acceder a la
información de la unión, debe hacerse con referencia a un miembro de
un tipo de dato adecuado y coherente con el último que se empleó. No
tendría sentido almacenar un dato de tipo float de uno de los campos
de la unión y luego querer leerlo a través de un campo de tipo char. El
resultado de una operación de este estilo es imprevisible.

Veamos un ejemplo, y comparémoslo con una estructura de definición


similar:
#include <stdio.h>
#include <string.h>
typedef union
{
long dni; // número de dni.
char ss[30]; // número de la seg. social.
}ident1;

typedef struct
{
long dni; // número de dni.
char ss[30]; // número de la seg. social.
}ident2;

void main(void)
{
ident1 id1;
ident2 id2;

printf("tamaño de la union: %ld\n",sizeof(ident1));


printf("tamaño de la estructura:%ld\n",sizeof(ident2));

// Datos de la estructura ...

345
Fundamentos de informática. Programación en Lenguaje C

id2.dni = 44561098;
*(id2.ss + 0) = NULL;
strcat(id2.ss,"12/0324/5431890");
printf("\nid2.dni = %ld\n",id2.dni);
printf("id2.ss = %s\n",id2.ss);

// Datos de la unión ...


*(id1.ss + 0) = NULL;
strcat(id1.ss,"12/0324/5431890");
printf("\nid1.dni = %ld (mal)\n",id1.dni);// Mal.
printf("id1.ss = %s\n",id1.ss);
id1.dni = 44561098;
printf("\nid1.dni = %ld\n",id1.dni);
printf("id1.ss = %s(mal)\n",id1.ss); // Mal.
}

El programa ofrece la siguiente salida por pantalla;


tamaño de la union: 30
tamaño de la estructura:34

id2.dni = 44561098
id2.ss = 12/0324/5431890

id1.dni = 808399409 (mal)


id1.ss = 12/0324/5431890

id1.dni = 44561098
id1.ss = ╩‗º☻324/5431890 (mal)

El tamaño de la estructura es la suma del tamaño de sus miembros. El


tamaño de la unión es el tamaño del mayor de sus miembros.

En la estructura se tienen espacios disjuntos para cada miembro: por un


lado se almacena el valor long de la variable dni y por otro la cadena de
caracteres ss. En la unión, si la última asignación se ha realizado sobre
la cadena, no tiene sentido que se pretenda obtener el valor del
miembro long dni; Y si la última asignación se ha realizado sobre el
campo dni, tampoco tiene sentido pretender leer el valor de la cadena
ss.

Una buena prueba de que la unión comparte la memoria la tenemos en


el ejemplo donde ha quedado como mal uso la impresión del dni. Si
vemos el valor impreso (808399409) y lo pasamos a hexadecimal
tenemos 302F3231. Y si separamos esa cifra en pares, tendremos

346
Capítulo 12. Estructuras estáticas de datos y definición de tipos.

cuatro pares: el 30 (que es el código ASCII del carácter ‘0’); el 2F (que


es el código ASCII del carácter ‘/’); el 32 (que es el código ASCII del
carácter ‘2’); y el 31, que es el código ASCII del carácter ‘1’). Y si vemos
el valor de los cuatro elementos de la cadena, tenemos que son “12/0”.
Precisamente los cuatro que antes hemos reconocido en la codificación
del mal leído entero. Y es que hemos leído los cuatro primeros
caracteres de la cadena como si de un entero se tratase.

En este aspecto, una buena utilidad del mal uso de las uniones (en ese
caso no sería mal uso: sería una treta del programador) será el poder
obtener el código interno de la información de los valores de tipo float.
Veamos como podríamos hacerlo:
#include <stdio.h>
typedef union
{
float fvalor;
unsigned long lvalor;
}codigo;

void main(void)
{
unsigned long Test = 0x80000000;
codigo a;

printf("Introduzca float del que desea ");


printf("conocer su codificacion binaria ... ");
scanf("%f", &a.fvalor);

while(Test)
{
printf("%c",a.lvalor & Test ? '1' : '0');
Test >>= 1;
}
}

La función principal lee el valor y lo almacena en el campo de tipo float.


Pero luego lo lee como si fuese un dato de tipo long. Y si a ese valor
long le aplicamos el operador and a nivel de bit (permitido en las
variables long, y no permitido en las float) llegamos a poder obtener la
codificación interna de los valores en coma flotante. No hemos explicado
en este manual la norma IEEE 754 que emplean los PC para codificar

347
Fundamentos de informática. Programación en Lenguaje C

esa clase de valores, pero quien quiera conocerla y cotejarla bien puede
hacerlo: la norma está fácilmente accesible; y el programa que
acabamos de presentar permite la visualización de esa codificación.

Ejercicios

68. Definir un tipo de dato entero, de longitud tan grande como se


quiera, y definir también los operadores básicos de ese nuevo
tipo de dato mediante la definición y declaración de las
funciones que sean necesarias.

La estructura que define el nuevo tipo de dato podría ser como la que
sigue:
#define Byte4 32
typedef unsigned long int UINT4;

typedef struct
{
/* número de elementos UINT4 reservados. */
UINT4 D;
/* número de elementos UINT4 utilizados actualmente. */
UINT4 T;
/* número de bits significativos. */
UINT4 B;
/* El número, que tendrá tantos elementos como indique D. */
UINT4 *N;
}NUMERO;

Que tiene cuatro elementos, que servirán para conocer bien las
propiedades del número (nuevo tipo de dato entero) y que nos
permitirán agilizar luego numerosas operaciones con ellos. El puntero N
recogerá un array (en asignación dinámica) donde se codificarán los
numeros; a este puntero se le asignan tantos elementos enteros largos
consecutivos como indique el campo D. Y una vez creado el número
(reservada la memoria), siempre convendrá mantener actualizado el

348
Capítulo 12. Estructuras estáticas de datos y definición de tipos.

valor de T y de B: el número de elementos enteros largos que realmente


se emplean en cada momento para la codificación del entero largo; y el
número de bits empleados en la codificación del número actual.

Podemos ahora definir una serie de operadores, empleando funciones.


Lo primero será definir la función que asigne memoria al puntero N y
aquella que ponga a cero el nuevo entero creado. A una la llamamos
PonerACero, y a la otra CrearNumero:
void PonerACero(NUMERO*n)
{
/* Esta función pone a cero los campos B y T de la variable
NUMERO recibida por referencia. Y deja también a cero todos
los dígitos del número. Considera que el campo T viene
correctamente actualizado. */
n->B = 0;
while(n->T) *(n->N + --n->T) = 0x0;
}

#define msg01_001 \
"01_001: Error de asignación de memoria en CrearNumero()\n"
void CrearNumero(NUMERO*n)
{
/* Con esta función asignamos una cantidad de memoria a n->N
(tantos elementos UINT4 como indique n->D) */

n->N = (UINT4*)malloc(n->D * sizeof(UINT4));


if(n->N == NULL)
{
printf(msg01_001);
exit(1);
}
n->T = n->D;
PonerACero(n);
}

Y podemos definir ahora otras funciones, necesarias para trabajar


cómodamente en este nuevo tipo de dato. Vamos introduciéndolas una a
una:

Función diseñada para copiar el valor de un NUMERO en otro


NUMERO. No bastaría hacer copia mediante el asignación dirección,
porque en ese caso se copiaría la dirección de N de uno a otro pero lo

349
Fundamentos de informática. Programación en Lenguaje C

que nos interesa es que sean variables diferentes con direcciones


diferentes, que codifiquen el mismo número; y se copiarían los valores
del campo D, y eso no nos interesa porque el campo D indica cuántos
elemento de tipo UINT4 se han reservado en el puntero, que depende
de cada NUMERO. La función queda:
#define msg01_002 \
"01_002: No se puede hacer copia de la variable. Error en
CopiarNumero()\n"
void CopiarNumero(NUMERO*original,NUMERO*copia)
{
/* Esta función copia el valor de número grande original->N
en copia->N. Previo a esta operación, verifica que el tamaño
del original no supera la capacidad (copia->D) de la copia.*/

UINT4 c;
if(original->T > copia->D)
{
printf(msg01_002);
exit(1);
}
/* Si el original y la copia no son la misma variable ... */
if(original->N != copia->N)
{
PonerACero(copia);
for(c = 0 ; c < original->T ; c++)
*(copia->N + c) = *(original->N + c);
copia->T = original->T;
copia->B = original->B;
}
}

Función que actualice los valores de los campos B y T:


void longitud(NUMERO*n)
{
/* De entrada se le supone el tamaño de la dimensión ----- */
n->T = n->D;
while(*(n->N + n->T - 1) == 0 && n->T != 0)
(n->T)--;
/* Una vez tenemos determinado el número de elementos UINT4
que forman el número, vamos ahora a calcular el número de
bits a partir del más significativo. */
n->B = Byte4 * n->T;
if(n->B)
{
UINT4 Test = 0x80000000;
while(!(*(n->N + n->T - 1) & Test))
{

350
Capítulo 12. Estructuras estáticas de datos y definición de tipos.

Test >>= 1;
n->B--;
}
}
}

La siguiente función lo que hace es intercambiar los valores de


dos variables NUMERO:
#define msg01_003 \
"01_003: No se pueden intercambiar valores. Error en
inv_v()\n"
void inv_v(NUMERO*n1,NUMERO*n2)
{
/* Si los dos tamaños de los arrays son iguales, entonces
intercambiamos los punteros de los números. */
if(n1->D == n2->D)
{
UINT4*aux;
aux = n1->N;
n1->N = n2->N;
n2->N = aux;
}
/* En caso contrario ... */
else
{
UINT4 L;
L = (n1->T >= n2->T) ? n1->T : n2->T;
if(n1->T > n2->D || n2->T > n1->D)
{
printf(msg01_003);
exit(1);
}
/* Intercambiamos cada uno de los dígitos */
while(L)
{
L--;
*(n1->N + L) ^= *(n2->N + L);
*(n2->N + L) ^= *(n1->N + L);
*(n1->N + L) ^= *(n2->N + L);
}
}
/* Intercambiamos los valores de los tamaños. */
n1->T ^= n2->T;
n2->T ^= n1->T;
n1->T ^= n2->T;
/* Intercambiamos los valores del número de bits. */
n1->B ^= n2->B;
n2->B ^= n1->B;
n1->B ^= n2->B;

351
Fundamentos de informática. Programación en Lenguaje C

/* Evidentemente, no podemos intercambiar las dimensiones en


que han sido definidos cada uno de los dos números grandes.*/
}

Y la siguiente función determina cuál de los dos NUMERO’s que


se reciben como parámetro es mayor y cuál menor:
typedef signed short int SINY2;
typedef signed long int SINT4;
SINT2 orden(NUMERO*n1,NUMERO*n2)
{
/* Devuelve +1 si n1 > n2.
-1 si n1 < n2.
0 si n1 == n2. */
SINT4 c;
if(n1->B < n2->B)
return -1; /* Es decir, n1 < n2. */
if(n1->B > n2->B)
return +1; /* Es decir, n1 > n2. */
/* LLegados aquí tenemos que *n1->T es igual a n2->T... */
for(c = n1->T - 1 ; c >= 0 ; c--)
{
if(*(n1->N + c) < *(n2->N + c))
return -1; /* Es decir, n1 < n2. */
if(*(n1->N + c) > *(n2->N + c))
return +1; /* Es decir, n1 > n2. */
}
return 0;
}

Podemos seguir definiendo funciones auxiliares, como desplazamiento a


izquierda o desplazamiento a derecha, etc. Lo dejamos como ejercicio
para resolver. También queda pendiente trabajar las operaciones
aritméticas de suma, resta, producto, cociente, módulo, etc. No es
objeto del libro mostrar todo ese código. Hemos mostrado el que
precede porque está formado en su mayor parte por funciones sencillas
y fáciles de implementar y porque todas ellas trabajan con estructuras.

352
CAPÍTULO 13
GESTIÓN DE ARCHIVOS

Hasta el momento, toda la información (datos) que hemos sido capaces


de gestionar, la hemos tomado de dos únicas fuentes: o eran datos del
programa, o eran datos que introducía el usuario desde el teclado. Y
hasta el momento, siempre que un programa ha obtenido un resultado,
lo único que hemos hecho ha sido mostrarlo en pantalla.

Y, desde luego, sería muy interesante poder almacenar la información


generada por un programa, de forma que esa información pudiera luego
ser consultada por otro programa, o por el mismo u otro usuario. O
sería muy útil que la información que un usuario va introduciendo por
consola quedase almacenada para sucesivas ejecuciones del programa o
para posibles manipulaciones de esa información.

En definitiva, sería muy conveniente poder almacenar en algún soporte


informático (por ejemplo, en el disco del ordenador) esa información, y
poder luego acceder a ese disco para volver a tomarla, para actualizar la
Fundamentos de informática. Programación en Lenguaje C

información almacenada, para añadir o para eliminar todo o parte de


ella.

Y eso es lo que vamos a ver en este tema: la gestión de archivos.


Comenzaremos con una breve presentación de carácter teórico sobre los
archivos y pasaremos a ver después el modo en que podemos emplear
los distintos formatos de archivo.

Tipos de dato con persistencia


Entendemos por tipo de dato con persistencia, o archivo, o fichero
aquel cuyo tiempo de vida no está ligado al de ejecución del programa
que lo crea o lo maneja. Es decir, se trata de una estructura de datos
externa al programa, que lo trasciende. Un archivo existe desde que un
programa lo crea y mientras que no sea destruido por este u otro
programa.

Un archivo está compuesto por registros homogéneos que llamamos


registros de archivo. La información de cada registro viene recogida
mediante campos.

Es posible crear ese tipo de dato con persistencia porque esa


información queda almacenada sobre una memoria externa. Los
archivos se crean sobre dispositivos de memoria masiva. El límite de
tamaño de un archivo viene condicionado únicamente por el límite de los
dispositivos físicos que lo albergan.

Los programas trabajan con datos que residen en la memoria principal


del ordenador. Para que un programa manipule los datos almacenados
en un archivo y, por tanto, en un dispositivo de memoria masiva, esos
datos deben ser enviados desde esa memoria externa a la memoria
principal mediante un proceso de extracción. Y de forma similar,
cuando los datos que manipula un programa deben ser concatenados
con los del archivo se utiliza el proceso de grabación.

354
Capítulo 13. Gestión de archivos.

De hecho, los archivos se conciben como estructuras que gozan de las


siguientes características:

1. Capaces de contener grandes cantidades de información.

2. Capaces de y sobrevivir a los procesos que lo generan y utilizan.

3. Capaces de ser accedidos desde diferentes procesos o programas.

Desde el punto de vista físico, o del hardware, un archivo tiene una


dirección física: en el disco toda la información se guarda (grabación) o
se lee (extracción) en bloques unidades de asignación o «clusters»
referenciados por un nombre de unidad o disco, la superficie a la que se
accede, la pista y el sector: todos estos elementos caracterizan la
dirección física del archivo y de sus elementos. Habitualmente, sin
embargo, el sistema operativo simplifica mucho esos accesos al archivo,
y el programador puede trabajar con un concepto simplificado de
archivo o fichero: cadena de bytes consecutivos terminada por un
carácter especial llamado EOF (“End Of File”); ese carácter especial
(EOF) indica que no existen más bytes de información más allá de él.

Este segundo concepto de archivo permite al usuario trabajar con datos


persistentes sin tener que estar pendiente de los problemas físicos de
almacenamiento. El sistema operativo posibilita al programador trabajar
con archivos de una forma sencilla. El sistema operativo hace de interfaz
entre el disco y el usuario y sus programas.

1. Cada vez que accede a un dispositivo de memoria masiva para leer o


paras grabar, el sistema operativo transporta, desde o hasta la
memoria principal, una cantidad fija de información, que se llama
bloque o registro físico y que depende de las características físicas
del citado dispositivo. En un bloque o registro físico puede haber
varios registros de archivo, o puede que un registro de archivo ocupe
varios bloques. Cuantos más registros de archivo quepan en cada
bloque menor será el número de accesos necesarios al dispositivo de

355
Fundamentos de informática. Programación en Lenguaje C

almacenamiento físico para lograr procesar toda la información del


archivo.

2. El sistema operativo también realiza la necesaria transformación de


las direcciones: porque una es la posición real o efectiva donde se
encuentra el registro dentro del soporte de información (dirección
física o dirección hardware) y otra distinta es la posición relativa
que ocupa el registro en nuestro archivo, tal y como es visto este
archivo por el programa que lo manipula (dirección lógica o
simplemente dirección).

3. Un archivo es una estructura de datos externa al programa.


Nuestros programas acceden a los archivos para leer, modificar,
añadir, o eliminar registros. El proceso de lectura o de escritura
también lo gobierna el sistema operativo. Al leer un archivo desde un
programa, se transfiere la información, de bloque en bloque, desde
el archivo hacia una zona reservada de la memoria principal llamada
buffer, y que está asociada a las operaciones de entrada y salida de
archivo. También se actúa a través del buffer en las operaciones de
escritura sobre el archivo.

Archivos y sus operaciones


Antes de abordar cómo se pueden manejar los archivos en C, será
conveniente hacer una breve presentación sobre los archivos con los
que vamos a trabajar: distintos modos en que se pueden organizar, y
qué operaciones se pueden hacer con ellos en función de su modo de
organización.

Hay diferentes modos de estructurar o de organizar un archivo. Las


características del archivo y las operaciones que con él se vayan a poder
realizar dependen en gran medida de qué modo de organización se
adopte. Las dos principales formas de organización que vamos a ver en
este manual son:

356
Capítulo 13. Gestión de archivos.

1. Secuencial. Los registros se encuentran en un orden secuencial, de


forma consecutiva. Los registros deben ser leídos, necesariamente,
según ese orden secuencial. Es posible leer o escribir un cierto
número de datos comenzando siempre desde el principio del archivo.
También es posible añadir datos a partir del final del archivo. El
acceso secuencial es una forma de acceso sistemático a los datos
poco eficiente si se quiere encontrar un elemento particular.

2. Indexado. Se dispone de un índice para obtener la ubicación de


cada registro. Eso permite localizar cualquier registro del archivo sin
tener que leer todos los que le preceden.

La decisión sobre cuál de las dos formas de organización tomar


dependerá del uso que se dé al archivo.

Para poder trabajar con archivos secuenciales, se debe previamente


asignar un nombre o identificador a una dirección de la memoria externa
(a la que hemos llamado antes dirección hardware). Al crear ese
identificador se define un indicador de posición de archivo que se coloca
en esa dirección inicial. Al iniciar el trabajo con un archivo, el indicador
se coloca en el primer elemento del archivo que coincide con la dirección
hardware del archivo.

Para extraer un registro del archivo, el indicador debe previamente estar


ubicado sobre él; y después de que ese elemento es leído o extraído, el
indicador se desplaza automáticamente al siguiente registro de la
secuencia.

Para añadir nuevos registros primero es necesario que el indicador se


posicione o apunte al final del archivo. Conforme el archivo va creciendo
de tamaño, a cada nuevo registro se le debe asignar nuevo espacio en
esa memoria externa.

Y si el archivo está realizando acceso de lectura, entonces no permite el


de escritura; y al revés: no se pueden utilizar los dos modos de acceso
(lectura y escritura) de forma simultánea.

357
Fundamentos de informática. Programación en Lenguaje C

Las operaciones que se pueden aplicar sobre un archivo secuencial son:

1. Creación de un nuevo archivo vacío. El archivo es una secuencia


vacía: ().

2. Adición de registros mediante buffer. La adición almacenar un


registro nuevo concatenado con la secuencia actual. El archivo pasa
a ser la secuencia (secuencia inicial + buffer). La información en un
archivo secuencial solo es posible añadirla al final del archivo.

3. Inicialización para comenzar luego el proceso de extracción. Con


esta operación se coloca el indicador sobre el primer elemento de la
secuencia, dispuesto así para comenzar la lectura de registros. El
archivo tiene entonces la siguiente estructura: Izquierda = ();
Derecha = (secuencia); Buffer = primer elemento de (secuencia).

4. Extracción o lectura de registros. Esta operación coloca el indicador


sobre el primer elemento o registro de la parte derecha del archivo y
concatena luego el primer elemento de la parte derecha al final de la
parte izquierda. Y eso de forma secuencial: para leer el registro n en
el archivo es preciso leer previamente todos los registros desde el 1
hasta el n – 1. Durante el proceso de extracción hay que verificar,
antes de cada nueva lectura, que no se ha llegado todavía al final del
archivo y que, por tanto, la parte derecha aún no es la secuencia
vacía.

Y no hay más operaciones. Es decir, no se puede definir ni la operación


inserción de registro, ni la operación modificación de registro, ni la
operación borrado de registro. Al menos diremos que no se realizan
fácilmente. La operación de inserción se puede realizar creando de
hecho un nuevo archivo. La modificación se podrá hacer si al realizar la
modificación no se aumenta la longitud del registro. Y el borrado no es
posible y, por tanto, en los archivos secuenciales se define el borrado
lógico: marcar el registro de tal forma que esa marca se interprete como
elemento borrado.

358
Capítulo 13. Gestión de archivos.

Archivos de texto y binarios


Decíamos antes que un archivo es un conjunto de bytes secuenciales,
terminados por el carácter especial EOF.

Si nuestro archivo es de texto, esos bytes serán interpretados como


caracteres. Toda la información que se puede guardar en un archivo de
texto son caracteres. Esa información podrá por tanto ser visualizada
por un editor de texto.

Si se desean almacenar los datos de una forma más eficiente, se puede


trabajar con archivos binarios. Los números, por ejemplo, no se
almacenan como cadenas de caracteres, sino según la codificación
interna que use el ordenador. Esos archivos binarios no pueden
visualizarse mediante un editor de texto.

Si lo que se desea es que nuestro archivo almacene una información


generada por nuestro programa y que luego esa información pueda ser,
por ejemplo, editada, entonces se deberá trabajar con ficheros de
caracteres o de texto. Si lo que se desea es almacenar una información
que pueda luego ser procesada por el mismo u otro programa, entonces
es mejor trabajar con ficheros binarios.

Tratamiento de archivos en el lenguaje C


Ya hemos visto todas las palabras reservadas de C. Ninguna de ellas
hace referencia a operación alguna de entrada o salida de datos. Todas
las operaciones de entrada y salida están definidas mediante funciones
de biblioteca estándar.

Para trabajar con archivos con buffer, las funciones, tipos de dato
predefinidos y constantes están recogidos en la biblioteca stdio.h. Para
trabajar en entrada y salida de archivos sin buffer están las funciones
definidas en io.h.

359
Fundamentos de informática. Programación en Lenguaje C

Todas las funciones de stdio.h de acceso a archivo trabajan mediante


una interfaz que está localizada por un puntero. Al crear un archivo, o al
trabajar con él, deben seguirse las normas que dicta el sistema
operativo. De trabajar así se encargan las funciones ya definidas, y esa
gestión es transparente para el programador.

Esa interfaz permite que el trabajo de acceso al archivo sea


independiente del dispositivo final físico donde se realizan las
operaciones de entrada o salida. Una vez el archivo ha quedado abierto,
se puede intercambiar información entre ese archivo y el programa. El
modo en que la interfaz gestiona y realiza ese tráfico es algo que no
afecta para nada a la programación.

Al abrir, mediante una función, un archivo que se desee usar, se indica,


mediante un nombre, a qué archivo se quiere acceder; y esa función de
apertura devuelve al programa una dirección que deberá emplearse en
las operaciones que se realicen con ese archivo desde el programa. Esa
dirección se recoge en un puntero, llamado puntero de archivo. Es un
puntero a una estructura que mantiene información sobre el archivo: la
dirección del buffer, el código de la operación que se va a realizar, etc.
De nuevo el programador no se debe preocupar de esos detalles:
simplemente debe declarar en su programa un puntero a archivo, como
ya veremos más adelante.

El modo en que las funciones estándar de ANSI C gestionan todo el


acceso a disco es algo transparente al programador. Cómo trabaja
realmente el sistema operativo con el archivo sigue siendo algo que no
afecta al programador. Pero es necesario que de la misma manera que
una función de ANSI C ha negociado con el sistema operativo la
apertura del archivo y ha facilitado al programador una dirección de
memoria, también sea una función de ANSI C quien cierre al final del
proceso los archivos abiertos, de forma también transparente para el
programador. Si se interrumpe inesperadamente la ejecución de un
programa, o éste termina sin haber cerrado los archivos que tiene

360
Capítulo 13. Gestión de archivos.

abiertos, se puede sufrir un daño irreparable sobre esos archivos, y


perderlos o perder parte de su información.

También es transparente al programador el modo en que se accede de


hecho a la información del archivo. El programa no accede nunca al
archivo físico, sino que actúa siempre y únicamente sobre la memoria
intermedia o buffer, que es el lugar de almacenamiento temporal de
datos. Únicamente se almacenan los datos en el archivo físico cuando la
información se transfiere desde el buffer hasta el disco. Y esa
transferencia no necesariamente coincide con la orden de escritura o
lectura que da el programador. De nuevo, por tanto, es muy importante
terminar los procesos de acceso a disco de forma regular y normalizada,
pues de lo contrario, si la terminación del programa se realiza de forma
anormal, es muy fácil que se pierdan al menos los datos que estaban
almacenados en el buffer y que aún no habían sido, de hecho,
transferidos a disco.

Archivos secuenciales con buffer.


Antes de utilizar un archivo, la primera operación, previa a cualquier
otra, es la de apertura.

Ya hemos dicho que cuando abrimos un archivo, la función de apertura


asignará una dirección para ese archivo. Debe por tanto crearse un
puntero para recoger esa dirección.

En la biblioteca stdio.h está definido el tipo de dato FILE, que es tipo


de dato puntero a archivo. Este puntero nos permite distinguir entre los
diferentes ficheros abiertos en el programa. Crea la secuencia o interfaz
que nos permite la transferencia de información con el archivo
apuntado.

La sintaxis para la declaración de un puntero a archivo es la siguiente:

FILE *puntero_a_archivo;

361
Fundamentos de informática. Programación en Lenguaje C

Vamos ahora a ir viendo diferentes funciones definidas en stdio.h para


la manipulación de archivos.

• Apertura de archivo.

La función fopen abre un archivo y devuelve un puntero asociado al


mismo, que puede ser utilizado para que el resto de funciones de
manipulación de archivos accedan a este archivo abierto.

Su prototipo es:

FILE *fopen(const char*nombre_archivo, const char


*modo_apertura);

Donde nombre_archivo es el nombre del archivo que se desea abrir.


Debe ir entre comillas dobles, como toda cadena de caracteres. El
nombre debe estar consignado de tal manera que el sistema operativo
sepa identificar el archivo de qué se trata.

Y donde modo_apertura es el modo de acceso para el que se abre el


archivo. Debe ir en comillas dobles. Los posibles modos de apertura de
un archivo secuencial con buffer son:

“r” Abre un archivo de texto para lectura. El archivo debe existir.

“w” Abre un archivo de texto para escritura. Si existe ese archivo,


lo borra y lo crea de nuevo. Los datos nuevos se escriben
desde el principio.

“a” Abre un archivo de texto para escritura. Los datos nuevos se


añaden al final del archivo. Si ese archivo no existe, lo crea.

“r+” Abre un archivo de texto para lectura/escritura. Los datos se


escriben desde el principio. El fichero debe existir.

“w+” Abre un archivo de texto para lectura/escritura. Los datos se


escriben desde el principio. Si el fichero no existe, lo crea.

362
Capítulo 13. Gestión de archivos.

“rb” Abre un archivo binario para lectura. El archivo debe existir.

“wb” Abre un archivo binario para escritura. Si existe ese archivo, lo


borra y lo crea de nuevo. Los datos nuevos se escriben desde
el principio.

“ab” Abre un archivo binario para escritura. Los datos nuevos se


añaden al final del archivo. Si ese archivo no existe, lo crea.

“r+b” Abre un archivo binario para lectura/escritura. Los datos se


escriben desde el principio. El fichero debe existir.

“w+b” Abre un archivo binario para lectura/escritura. Los datos se


escriben desde el principio. Si el fichero no existe, lo crea.

Ya vemos que hay muy diferentes formas de abrir un archivo. Queda


claro que de todas ellas destacan dos bloques: aquellas que abren el
archivo para manipular una información almacenada en binario, y otras
que abren el archivo para poder manipularlo en formato texto. Ya
iremos viendo ambas formas de trabajar la información a medida que
vayamos presentando las distintas funciones.

La función fopen devuelve un puntero a una estructura que recoge las


características del archivo abierto. Si se produce algún error en la
apertura del archivo, entonces la función fopen devuelve un puntero
nulo.

Ejemplos simples de esta función serían:


FILE *fichero;
fichero = fopen(“datos.dat”,”w”);

Que deja abierto el archivo datos.dat para escritura. Si ese archivo ya


existía, queda eliminado y se crea otro nuevo y vacío.

El nombre del archivo puede introducirse mediante variable:


char nombre_archivo[80];
printf(“Indique el nombre del archivo ... ”);

363
Fundamentos de informática. Programación en Lenguaje C

gets(nombre_archivo);
fopen(nombre_archivo, “w”);

Y ya hemos dicho que si la función fopen no logra abrir el archivo,


entonces devuelve un puntero nulo. Es muy conveniente verificar
siempre que el fichero ha sido realmente abierto y que no ha habido
problemas:
FILE *archivo;
if(archivo = fopen(“datos.dat”, “w”) == NULL)
printf(“No se puede abrir el archivo\n”);

Dependiendo del compilador se podrán tener más o menos archivos


abiertos a la vez. En todo caso, siempre se podrán tener, al menos ocho
archivos abiertos simultáneamente.

• Cierre del archivo abierto.

La función fclose cierra el archivo que ha sido abierto mediante fopen.


Su prototipo es el siguiente:

int fclose(FILE *nombre_archivo);

La función devuelve el valor cero si ha cerrado el archivo correctamente.


Un error en el cierre de un archivo puede ser fatal y puede generar todo
tipo de problemas. El más grave de ellos es el de la pérdida parcial o
total de la información del archivo.

Cuando una función termina normalmente su ejecución, cierra de forma


automática todos sus archivos abiertos. De todas formas es conveniente
cerrar los archivos cuando ya no se utilicen dentro de la función, y no
mantenerlos abiertos en espera de que se finalice su ejecución.

• Escritura de un carácter en un archivo.

Existen dos funciones definidas en stdio.h para escribir un carácter en


el archivo. Ambas realizan la misma función y ambas se utilizan
indistintamente. La duplicidad de definición es necesaria para preservar
la compatibilidad con versiones antiguas de C.

Los prototipos de ambas funciones son:

364
Capítulo 13. Gestión de archivos.

int putc(int c, FILE *nombre_archivo);

int fputc(int c, FILE * nombre_archivo);

Donde nombre_archivo recoge la dirección que ha devuelto la función


fopen. El archivo debe haber sido abierto para escritura y en formato
texto. Y donde la variable c es el carácter que se va a escribir. Por
razones históricas, ese carácter se define como un entero, pero de esos
dos o cuatro bytes (dependiendo de la longitud de la palabra) sólo se
toma en consideración el menos significativo.

Si la operación de escritura se realiza con éxito, la función devuelve el


mismo carácter escrito.

Vamos a hacer un programa que solicite al usuario su nombre y


entonces guarde ese dato en un archivo que llamaremos nombre.dat.
#include <stdio.h>
#include <stdlib.h>
void main(void)
{
char nombre[80];
short int i;
FILE *archivo;

printf("Su nombre ... ");


gets(nombre);

archivo = fopen("nombre.dat", "w");


if(archivo == NULL)
{
printf("No se ha podido abrir el archivo.\n");
printf("Pulse una tecla para finalizar... ");
getchar();
exit(1);
}
i = 0;
while(nombre[i] != NULL)
{
fputc(nombre[i],archivo);
i++;
}
fclose(archivo);
}

365
Fundamentos de informática. Programación en Lenguaje C

Una vez ejecutado el programa, y si todo ha ido correctamente, se


podrá abrir el archivo nombre.dat con un editor de texto y comprobar
que realmente se ha guardado el nombre en ese archivo.

• Lectura de un carácter desde un archivo.

De manera análoga a las funciones de escritura, existen también


funciones de lectura de caracteres desde un archivo. De nuevo hay dos
funciones equivalentes, cuyos prototipos son:

int fgetc(FILE *nombre_archivo);

int getc(FILE * nombre_archivo);

Que reciben como parámetro el puntero devuelto por la función fopen al


abrir el archivo y devuelven el carácter, de nuevo como un entero. El
archivo debe haber sido abierto para lectura y en formato texto. Cuando
ha llegado al final del archivo, la función fgetc, o getc, devuelve una
marca de fin de archivo que se codifica como EOF.

El código para leer el nombre desde el archivo donde ha quedado


almacenado en el programa anterior sería:
#include <stdio.h>
#include <stdlib.h>
void main(void)
{
char nombre[80];
short int i;
FILE *archivo;

archivo = fopen("nombre.dat", "r");


if(archivo == NULL)
{
printf("No se ha podido abrir el archivo.\n");
printf("Pulse una tecla para finalizar... ");
getchar();
exit(1);
}
i = 0;
while((nombre[i++] = fgetc(archivo)) != EOF);
/* El último elemento de la cadena ha quedado igual
a EOF. Se cambia al carácter fin de cadena, NULL */
nombre[--i] = NULL;
fclose(archivo);

366
Capítulo 13. Gestión de archivos.

printf("Su nombre ... %s", nombre);


}

Este código mostrará por pantalla el nombre almacenado en el archivo


nombre.dat.

• Lectura y escritura de una cadena de caracteres.

Las funciones fputs y fgets escriben y leen, respectivamente, cadenas


de caracteres sobre archivos de disco.

Sus prototipos son:

int fputs(const char *s, FILE *nombre_archivo);

char *fgets(char *s, int n, FILE * nombre_archivo);

La función fputs escribe la cadena s en el archivo indicado por el


puntero nombre_archivo. Si la operación ha sido correcta, devuelve un
valor no negativo. El archivo debe haber sido abierto en formato texto y
para escritura o para lectura, dependiendo de la función que se emplee.

La función fgets lee del archivo indicado por el puntero nombre_archivo


una cadena de caracteres. Lee los caracteres desde el inicio hasta un
total de n, que es el valor que recibe como segundo parámetro. Si antes
del carácter n-ésimo ha terminado la cadena, también termina la lectura
y cierra la cadena con un carácter nulo.

En el programa que vimos para la función fputc podríamos eliminar la


variable i y cambiar la estructura while por la sentencia:

fputs(nombre,archivo);

Y en el programa que vimos para la función fgetc, la sentencia podría


quedar sencillamente:

fgets(nombre, 80, archivo);

• Lectura y escritura formateada.

367
Fundamentos de informática. Programación en Lenguaje C

Las funciones fprintf y fscanf de entrada y salida de datos por disco


tienen un uso semejante a las funciones printf y scanf, de entrada y
salida por consola.

Sus prototipos son:

int fprintf(FILE *nombre_archivo, const char *cadena_formato [,


argumento, ...]);

int fscanf(FILE *nombre_archivo, const char *cadena_formato [,


dirección, ...]);

Donde nombre_archivo es el puntero a archivo que devuelve la


función fopen. Los demás argumentos de estas dos funciones ya los
conocemos, pues son los mismos que las funciones de entrada y salida
por consola. La función fscanf devuelve el carácter EOF si ha llegado al
final del archivo. El archivo debe haber sido abierto en formato texto y
para escritura o para lectura, dependiendo de la función que se emplee.

Veamos un ejemplo de estas dos funciones. Hagamos un programa que


guarde en un archivo (que llamaremos numeros.dat”) los valores que
previamente se han asignado de forma aleatoria a un vector de
variables float. Esos valores se almacenan dentro de una cadena de
texto. Y luego, el programa vuelve a abrir el archivo para leer los datos
y cargarlos en otro vector y los muestra en pantalla.
#include <stdio.h>
#include <stdlib.h>
#define TAM 10
void main(void)
{
float or[TAM], cp[TAM];
short i;
FILE *ARCH;
char c[100];

randomize();
for(i = 0 ; i < TAM ; i++)
or[i] = (float)random(1000) / random(100);

ARCH = fopen("numeros.dat", "w");


if(ARCH == NULL)

368
Capítulo 13. Gestión de archivos.

{
printf("No se ha podido abrir el archivo.\n");
printf("Pulse una tecla para finalizar... ");
getchar();
exit(1);
}
for(i = 0 ; i < TAM ; i++)
fprintf(ARCH,"Valor %04hi-->%12.4f\n",i,or[i]);

fclose(ARCH);

ARCH = fopen("numeros.dat", "r");


if(ARCH == NULL)
{
printf("No se ha podido abrir el archivo.\n");
printf("Pulse una tecla para finalizar... ");
getchar();
exit(1);
}

printf("Los valores guardados en el archivo son:\n");


i = 0;
while(fscanf(ARCH,"%s%s%s%f",c,c,c,cp + i++)!= EOF);
for(i = 0 ; i < TAM ; i++)
printf("Valor %04hd --> %12.4f\n",i,cp[i]);
fclose(ARCH);
}

El archivo contiene (en una ejecución cualquiera: los valores son


aleatorios, y en cada ejecución llegaremos a valores diferentes) la
siguiente información:
Valor 0000 --> 9.4667
Valor 0001 --> 30.4444
Valor 0002 --> 12.5821
Valor 0003 --> 0.2063
Valor 0004 --> 16.4545
Valor 0005 --> 28.7308
Valor 0006 --> 9.9574
Valor 0007 --> 0.1039
Valor 0008 --> 18.0000
Valor 0009 --> 4.7018

Hemos definido la variable c para que vaya cargando desde el archivo


los tramos de cadena de caracteres que no nos interesan para la
obtención, mediante la función fscanf, de los sucesivos valores float
generados. Con esas tres lecturas de cadena la variable c va leyendo las
cadenas “Valor”; la cadena de caracteres que recoge el índice i; la

369
Fundamentos de informática. Programación en Lenguaje C

cadena”-->”. La salida por pantalla tendrá la misma apariencia que la


obtenida en el archivo.

Desde luego, con la función fscanf es mejor codificar bien la información


del archivo, porque de lo contrario la lectura de datos desde el archivo
puede llegar a hacerse muy incómoda.

• Lectura y escritura en archivos binarios.

Ya hemos visto las funciones para acceder a los archivos secuenciales de


tipo texto. Vamos a ver ahora las funciones de lectura y escritura en
forma binaria.

Si en todas las funciones anteriores hemos requerido que la apertura del


fichero o archivo se hiciera en formato texto, ahora desde luego, para
hacer uso de las funciones de escritura y lectura en archivos binarios, el
archivo debe hacer sido abierto en formato binario.

Las funciones que vamos a ver ahora permiten la lectura o escritura de


cualquier tipo de dato.

Los prototipos son los siguientes:

size_t fread(void *buffer, size_t n_bytes, size_t contador, FILE


*nombre_archivo);

size_t fwrite(const void *buffer, size_t n_bytes, size_t contador,


FILE *nombre_archivo);

Donde buffer es un puntero a la región de memoria donde se van a


escribir los datos leídos en el archivo, o el lugar donde están los datos
que se desean escribir en el archivo. Habitualmente será la dirección de
una variable. n_bytes es el número de bytes que ocupa cada dato que
se va a leer o grabar, y contador indica el número de datos de ese
tamaño que se van a leer o grabar. El último parámetro es el de la
dirección que devuelve la función fopen cuando se abre el archivo.

370
Capítulo 13. Gestión de archivos.

Ambas funciones devuelven el número de elementos escritos o leídos.


Ese valor debe ser el mismo que el de la variable contador, a menos que
haya ocurrido un error.

Estas dos funciones son útiles para leer y escribir cualquier tipo de
información. Es habitual emplearla junto con el operador sizeof, para
determinar así la longitud (n_bytes) de cada elemento a leer o escribir.

El ejemplo anterior puede servir para ejemplificar ahora el uso de esas


dos funciones. El archivo “numeros.dat” será ahora de tipo binario. El
programa cargará en forma binaria esos valores y luego los leerá para
calcular el valor medio de todos ellos y mostrarlos por pantalla:
#include <stdio.h>
#include <stdlib.h>
#define TAM 10
void main(void)
{
float or[TAM], cp[TAM];
double suma = 0;
short i;
FILE *ARCH;

randomize();
for(i = 0 ; i < TAM ; i++)
or[i] = (float)random(1000) / random(100);

ARCH = fopen("numeros.dat", "wb");


if(ARCH == NULL)
{
printf("No se ha podido abrir el archivo.\n");
printf("Pulse una tecla para finalizar... ");
getchar();
exit(1);
}
fwrite(or,sizeof(float),TAM,ARCH);
fclose(ARCH);

ARCH = fopen("numeros.dat", "rb");


if(ARCH == NULL)
{
printf("No se ha podido abrir el archivo.\n");
printf("Pulse una tecla para finalizar... ");
getchar();
exit(1);
}
fread(cp,sizeof(float),TAM,ARCH);

371
Fundamentos de informática. Programación en Lenguaje C

fclose(ARCH);

for(i = 0 ; i < TAM ; i++)


{
printf("Valor %04hd --> %12.4f\n",i,cp[i]);
suma += *(cp + i);
}
printf("\n\nLa media es ... %lf", suma / TAM);
}

• Otras funciones útiles en el acceso a archivo

Función feof: Esta función (en realidad es una macro) determina el final
de archivo. Es conveniente usarla cuando se trabaja con archivos
binarios, donde se puede inducir a error y tomar como carácter EOF un
valor entero codificado.

Su prototipo es:

int feof(FILE *nombre_archivo);

que devuelve un valor diferente de cero si en la última operación de


lectura se ha detectado el valor EOF. en caso contrario devuelve el valor
cero.

Función ferror: Esta función (en realidad es una macro) determina si se


ha producido un error en la última operación sobre el archivo. Su
prototipo es:

int ferror(FILE * nombre_archivo);

Si el valor devuelto es diferente de cero, entonces se ha producido un


error; si es igual a cero, entonces no se ha producido error alguno.

Si deseamos hacer un programa que controle perfectamente todos los


accesos a disco, entonces convendrá ejecutar esta función después de
cada operación de lectura o escritura.

Función remove: Esta función elimina un archivo. El archivo será


cerrado si estaba abierto y luego será eliminado. Quiere esto decir que
el archivo quedará destruido, que no es lo mismo que quedarse vacío.

Su prototipo es:

372
Capítulo 13. Gestión de archivos.

int remove(const char * nombre_archivo);

Donde nombre_archivo es el nombre del archivo que se desea borrar.


En ese nombre, como siempre, debe ir bien consignada la ruta completa
del archivo. Un archivo así eliminado no es recuperable.

Por ejemplo, en nuestro ejemplos anteriores, después de haber hecho la


transferencia de datos al vector de float, podríamos ya eliminar el
archivo de nuestro disco. Hubiera abastado poner la sentencia:

remove("numeros.dat");

Si el archivo no ha podido ser eliminado (por denegación de permiso o


porque el archivo no existe en la ruta y nombre que ha dado el
programa) entonces la función devuelve el valor -1. Si la operación de
eliminación del archivo ha sido correcta, entonces devuelve un cero.

En realidad, la macro remove lo único que hace es invocar a la función


de borrado definida en io.h: la función unlink, cuyo prototipo es:

int unlink(const char *filename);

Y cuyo comportamiento es idéntico al explicado para la macro remove.

Entrada y salida sobre archivos de acceso aleatorio


Disponemos de algunas funciones que permiten acceder de forma
aleatoria a una u otra posición del archivo.

Ya dijimos que un archivo, desde e punto de vista del programador es


simplemente un puntero a la posición del archivo (en realidad al buffer)
donde va a tener lugar el próximo acceso al archivo. Cuando se abre el
archivo ese puntero recoge la dirección de la posición cero del archivo,
es decir, al principio. Cada vez que el programa indica escritura de
datos, el puntero termina ubicado al final del archivo.

Pero también podemos, gracias a algunas funciones definidas en io.h,


hacer algunos accesos aleatorios. En realidad, el único elemento nuevo

373
Fundamentos de informática. Programación en Lenguaje C

que se incorpora al hablar de acceso aleatorio es una función capaz de


posicionar el puntero del archivo devuelto por la función fopen en
distintas partes del fichero y poder así acceder a datos intermedios.

La función fseek puede modificar el valor de ese puntero, llevándolo


hasta cualquier byte del archivo y logrando así un acceso aleatorio. Es
decir, que las funciones estándares de ANSI C logran hacer accesos
aleatorios únicamente mediante una función que se añade a todas las
que ya hemos visto para los accesos secuenciales.

El prototipo de la función, definida en la biblioteca stdio.h es el


siguiente:

int fseek(FILE *nomnre_archivo, long despl, int modo);

Donde nombre_archivo es el puntero que ha devuelto la función fopen


al abrir el archivo; donde despl es el desplazamiento, en bytes, a
efectuar; y donde modo es el punto de referencia que se toma para
efectuar el desplazamiento. Para esa definición de modo, stdio.h define
tres constantes diferentes:

SEEK_SET, que es valor 0.

SEEK_CUR, que es valor 1,

SEEK_END, que es valor 2.

El modo de la función fseek puede tomar como valor cualquiera de las


tres constantes. Si tiene la primera (SEEK_SET), el desplazamiento se
hará a partir del inicio del fichero; si tiene la segunda (SEEK_CUR), el
desplazamiento se hará a partir de la posición actual del puntero; si
tiene la tercera (SEEK_END), el desplazamiento se hará a partir del final
del fichero.

Para la lectura del archivo que habíamos visto para ejemplificar la


función fscanf, las sentencias de lectura quedarían mejor si se hiciera
así:
printf("Los valores guardados en el archivo son:\n");
i = 0;

374
Capítulo 13. Gestión de archivos.

while(!feof(ARCH))
{
fseek(ARCH,16,SEEK_CUR);
fscanf(ARCH,"%f",cp + i++);
}

Donde hemos indicado 16 en el desplazamiento en bytes, porque 16 son


los caracteres que no deseamos que se lean en cada línea.

Los desplazamientos en la función fseek pueden ser positivos o


negativos. Desde luego, si los hacemos desde el principio lo razonable
es hacerlos positivos, y si los hacemos desde el final hacerlos negativos.
La función acepta cualquier desplazamiento y no produce nunca un
error. Luego, si el desplazamiento ha sido erróneo, y nos hemos
posicionado en medio de ninguna parte o en un byte a mitad de dato,
entonces la lectura que pueda hacer la función que utilicemos será
imprevisible.

Una última función que presentamos en este capítulo es la llamada


rewind, cuyo prototipo es:

void rewind(FILE *nombre_archivo);

Que “rebobina” el archivo, devolviendo el puntero a su posición inicial, al


principio del archivo.

Ejercicios

69. Descargar desde Internet un archivo con el texto completo de


El Quijote. Almacenarlo en formato texto. Darle a este archivo
el nombre quijote.txt. Y hacer entonces un programa que vaya
leyendo uno a uno los caracteres del archivo y vaya contando
cuántas veces aparece cada una de las letras del abecedario.
Mostrar al final en pantalla las veces que han aparecido cada
una de las letras y también el porcentaje de aparición respecto

375
Fundamentos de informática. Programación en Lenguaje C

al total de todas las letras aparecidas.

Vamos a ofrecer dos soluciones a este programa. La primera es la más


trivial:
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
void main(void)
{
long letra[27];
short caracter;
long suma = 0;
short int i;
FILE *archivo;

for(i = 0 ; i < 26 ; i++) letra[i] = 0;

archivo = fopen("quijote.txt", "r");


if(archivo == NULL)
{
printf("No se ha podido abrir el archivo.\n");
printf("Pulse una tecla para finalizar... ");
getchar();
exit(1);
}
while((caracter = fgetc(archivo)) != EOF)
{
if(isalpha(caracter))
{
i = (short)tolower(caracter) - (short)'a';
if(i >= 0 && i < 26) letra[i]++;
}
}

fclose(archivo);
for(i = 0 ; i < 26 ; i++) suma += letra[i];
for(i = 0 ; i < 26 ; i++)
{
printf("[ %c ]= %10ld\t",(char)(i+’A’),letra[i]);
printf("%7.2lf\n",((float)letra[i]/suma)*100);
}

printf("\n\nTotal letras ... %ld",suma);


}

376
Capítulo 13. Gestión de archivos.

Esta es la solución primera y sencilla. El vector letras tiene 26


elementos: tantos como letras tiene el abecedario ASCII. Pasamos
siempre la letra a minúscula porque así no hemos de verificar que nos
venga el carácter en mayúscula, y nos ahorramos muchas
comparaciones. El vector letra se indexa siempre por medio de la
variable i.

La pega es que con este código no sumamos las veces que aparecen las
vocales con acento, o la letra ‘u’ con diéresis. Y, desde luego, no
calculamos cuántas veces aparece la letra ‘Ñ’ ó la letra ‘ñ’. Para poder
hacer esos cálculos, deberemos modificar el programa añadiendo
algunas instrucciones:
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
void main(void)
{
long letra[27];
short caracter;
long suma = 0;
short int i;
FILE *archivo;

for(i = 0 ; i < 27 ; i++) letra[i] = 0;

archivo = fopen("quijote.txt", "r");


if(archivo == NULL)
{
printf("No se ha podido abrir el archivo.\n");
printf("Pulse una tecla para finalizar... ");
getchar();
exit(1);
}
while((caracter = fgetc(archivo)) != EOF)
{
if(caracter == 209 || caracter == 241)
letra[26]++; // letras ñ y Ñ
else if(caracter == 225 || caracter == 193)
letra['a' - 'a']++; // letras á y Á
else if(caracter == 233 || caracter == 201)
letra['e' - 'a']++; // letras é y É
else if(caracter == 237 || caracter == 205)
letra['i' - 'a']++; // letras í e Í
else if(caracter == 243 || caracter == 211)
letra['o' - 'a']++; // letras ó y Ó

377
Fundamentos de informática. Programación en Lenguaje C

else if(caracter == 250 || caracter == 218)


letra['u' - 'a']++; // letras ú y Ú
else if(caracter == 252 || caracter == 220)
letra['u' - 'a']++; // letras ü y Ü
else if(isalpha(caracter))
{
i = (short)tolower(caracter) - (short)'a';
if(i >= 0 && i < 26) letra[i]++;
}
}

fclose(archivo);
for(i = 0 ; i < 27 ; i++) suma += letra[i];
for(i = 0 ; i < 26 ; i++)
{
printf("[ %c ]= %10ld\t",(char)(i+’A’),letra[i]);
printf("%7.2lf\n",((float)letra[i]/suma)*100);
}
printf("[ %c ] = %10ld\t", 165,letra[26]);
printf("%7.2lf\n",((float)letra[26] / suma) * 100);

printf("\n\nTotal letras ... %ld",suma);


}

70. Implementar una base de datos de asignaturas. El programa


será muy sencillo, y simplemente debe definir una estructura
como la que ya estaba definida en un tema anterior. El
programa almacenará en disco y añadirá al final de archivo
cada una de las nuevas asignaturas que se añadan. La
información se guardará en binario. Se ofrecerá la posibilidad
de realizar un listado de todas las asignaturas por pantalla o
grabando ese listado en disco, creando un documento que se
pueda luego tratar con un programa editor de texto.

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

typedef struct
{
unsigned long clave;

378
Capítulo 13. Gestión de archivos.

char descr[50];
double cred;
}asignatura;

short mostrar_opciones(void);
void error(void);
short anyadir(char*);
short pantalla(char*);
short impresora(char*);

void main(void)
{
char nombre_archivo[80];
short opcion;
short oK;
printf("Nombre del archivo de asignaturas ... ");
gets(nombre_archivo);
do
{
opcion = mostrar_opciones();
switch(opcion)
{
case '1': oK = anyadir(nombre_archivo);
if(oK) error();
break;
case '2': oK = pantalla(nombre_archivo);
if(oK) error();
break;
case '3': oK = impresora(nombre_archivo);
if(oK) error();
break;
case '4': exit(1);
}
}while(1);
}

short mostrar_opciones(void)
{
char opcion;
clrscr();
printf("\n\n\t\tOpciones y Tareas");
printf("\n\n\t1. Añadir nueva asignatura.");
printf("\n\t2. Mostrar listado por pantalla.");
printf("\n\t3. Mostrar listado en archivo.");
printf("\n\t4. Salir del programa.");
printf("\n\n\t\t\tElegir opcion ... ");
do opcion = getchar(); while(opcion <'0'&&opcion >'4');
return opcion;
}

void error(void)

379
Fundamentos de informática. Programación en Lenguaje C

{
printf("Error en la operacion de acceso disco.\n");
printf("Pulse una tecla para terminar ... \n");
getchar();
exit(1);
}

short anyadir(char archivo[])


{
FILE *ARCH;
asignatura asig;
printf("\n\n\nDATOS DE LA NUEVA ASIGNATURA.\n\n");
printf("clave de la asignatura ... ");
scanf("%lu",&asig.clave);
printf("\nDescripcion ... ");
flushall();
gets(asig.descr);
printf("\nCreditos ...... ");
scanf("%lf",&asig.cred);
ARCH = fopen(archivo,"ab");
fwrite(&asig,sizeof(asig),1,ARCH);
printf("\n\n\tPulsar una tecla para continuar ... ");
getchar();
if(ferror(ARCH)) return 1;
fclose(ARCH);
return 0;
}

short pantalla(char archivo[])


{
FILE *ARCH;
asignatura asig;

ARCH = fopen(archivo,"a+b");
rewind(ARCH);
while(fread(&asig,sizeof(asig),1,ARCH) == 1)
{
printf("\n\nClave ......... %lu",asig.clave);
printf("\nDescripcion ... %s",asig.descr);
printf("\nCreditos ...... %6.1lf",asig.cred);
}
printf("\n\n\tPulsar una tecla para continuar ... ");
getchar();
if(ferror(ARCH)) return 1;
fclose(ARCH);
return 0;
}

short impresora(char archivo[])


{
FILE *ARCH1, *ARCH2;

380
Capítulo 13. Gestión de archivos.

asignatura asig;

ARCH1 = fopen(archivo,"rb");
ARCH2 = fopen("impresora","w");
while(fread(&asig,sizeof(asig),1,ARCH1) == 1)
{
fprintf(ARCH2,"\n\nClave\t%lu", asig.clave);
fprintf(ARCH2,"\nDescripcion \t%s", asig.descr);
fprintf(ARCH2,"\nCreditos\t%6.1lf", asig.cred);
}
printf("\n\n\tPulsar una tecla para continuar ... ");
getchar();
if(ferror(ARCH1)) return 1;
fclose(ARCH1);
fclose(ARCH2);
return 0;
}

La función principal presenta únicamente una estructura switch que


gestiona cuatro posibles valores para la variable opcion. Esos valores se
muestran en la primera de las funciones, la función mostrar_opciones,
que imprime en pantalla las cuatro posibles opciones del programa,
(añadir registros, mostrarlos por pantalla, crear un archivo de
impresión, y salir del programa) y devuelve a la función principal el
valor de la opción elegida.

La función anyadir recoge los valores de una nueva asignatura y guarda


la información, mediante la función fwrite, en el archivo que ha indicado
el usuario al comenzar la ejecución del programa. El archivo se abre
para añadir y para codificación binaria: “ab”. En esta función se invoca a
otra, llamada flushall. Esta función, de la biblioteca stdio.h, vacía
todos los buffers de entrada. La ejecutamos antes de la función gets
para variar el buffer de teclado. A veces ese buffer contiene algún
carácter, o el carácter intro pulsado desde la última entrada de datos
por teclado, y el sistema operativo lo toma como entrada de a función
gets, que queda ejecutada sin intervención del usuario.

La función pantalla muestra por pantalla un listado de las asignaturas


introducidas hasta el momento y guardadas en el archivo. Abre el
archivo para lectura en formato binario: “a+b”. No lo hemos abierto

381
Fundamentos de informática. Programación en Lenguaje C

como “rb” para evitar el error en caso de que el usuario quiera leer un
archivo inexistente.

La función impresora hace lo mismo que pantalla, pero en lugar de


mostrar los datos por la consola los graba en un archivo de texto. Por
eso esa función abre dos archivos y va grabando el texto en el archivo
abierto como “w”. Si el archivo “impresora” ya existe, entonces es
eliminado y crea otro en su lugar.

Se puede completar el programa con nuevas opciones. Se podría


modificar la función mostrar_opciones y la función main incorporando
esas opciones nuevas. Y se crearían las funciones necesarias para esas
nuevas tareas. Por ejemplo: eliminar el archivo de asignaturas; hacer
una copia de seguridad del archivo; buscar una asignatura en el archivo
cuya clave sea la que indique el usuario y, si la encuentra, entonces
muestre por pantalla la descripción y el número de créditos; etc.

382

También podría gustarte