Instruction manuals y Product Manuals">
Introduccion A La Programacion en C
Introduccion A La Programacion en C
Introduccion A La Programacion en C
con C
c 2003 de Andrés Marzal Varó e Isabel Gracia Luengo. Reservados todos los derechos.
Esta ((Edición Internet)) se puede reproducir con fines autodidactas o para su uso en
centros públicos de enseñanza, exclusivamente. En el segundo caso, únicamente se car-
garán al estudiante los costes de reproducción. La reproducción total o parcial con ánimo
de lucro o con cualquier finalidad comercial está estrictamente prohibida sin el permiso
escrito de los autores.
Índice general
1. Introducción a C 1
1.1. C es un lenguaje compilado . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
1.2. Traduciendo de Python a C: una guı́a rápida . . . . . . . . . . . . . . . . . . . . 5
1.3. Estructura tı́pica de un programa C . . . . . . . . . . . . . . . . . . . . . . . . . 12
1.4. C es un lenguaje de formato libre . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
1.5. Hay dos tipos de comentario . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
1.6. Valores literales en C . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
1.6.1. Enteros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
1.6.2. Flotantes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
1.6.3. Cadenas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
1.7. C tiene un rico juego de tipos escalares . . . . . . . . . . . . . . . . . . . . . . . . 23
1.7.1. El tipo int . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
1.7.2. El tipo unsigned int . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
1.7.3. El tipo float . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
1.7.4. El tipo char . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
1.7.5. El tipo unsigned char . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
1.8. Se debe declarar el tipo de toda variable antes de usarla . . . . . . . . . . . . . . 25
1.8.1. Identificadores válidos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
1.8.2. Sentencias de declaración . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
1.8.3. Declaración con inicialización . . . . . . . . . . . . . . . . . . . . . . . . . 26
1.9. Salida por pantalla . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
1.9.1. Marcas de formato para la impresión de valores con printf . . . . . . . . . 27
1.10. Variables y direcciones de memoria . . . . . . . . . . . . . . . . . . . . . . . . . . 31
1.11. Entrada por teclado . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
1.12. Expresiones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
1.13. Conversión implı́cita y explı́cita de tipos . . . . . . . . . . . . . . . . . . . . . . . 41
1.14. Las directivas y el preprocesador . . . . . . . . . . . . . . . . . . . . . . . . . . . 43
1.15. Constantes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44
1.15.1. Definidas con la directiva define . . . . . . . . . . . . . . . . . . . . . . . 44
1.15.2. Definidas con el adjetivo const . . . . . . . . . . . . . . . . . . . . . . . . 44
1.15.3. Con tipos enumerados . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44
1.16. Las bibliotecas (módulos) se importan con #include . . . . . . . . . . . . . . . . 47
1.16.1. La biblioteca matemática . . . . . . . . . . . . . . . . . . . . . . . . . . . 47
1.17. Estructuras de control . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
1.17.1. Estructuras de control condicionales . . . . . . . . . . . . . . . . . . . . . 49
1.17.2. Estructuras de control iterativas . . . . . . . . . . . . . . . . . . . . . . . 53
1.17.3. Sentencias para alterar el flujo iterativo . . . . . . . . . . . . . . . . . . . 59
3. Funciones 137
3.1. Definición de funciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 137
3.2. Variables locales y globales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 141
3.2.1. Variables locales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 141
3.2.2. Variables globales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 143
3.3. Funciones sin parámetros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 144
3.4. Procedimientos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 146
3.5. Paso de parámetros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 147
3.5.1. Parámetros escalares: paso por valor . . . . . . . . . . . . . . . . . . . . . 147
3.5.2. Organización de la memoria: la pila de llamadas a función . . . . . . . . . 147
3.5.3. Vectores de longitud variable . . . . . . . . . . . . . . . . . . . . . . . . . 153
3.5.4. Parámetros vectoriales: paso por referencia . . . . . . . . . . . . . . . . . 153
3.5.5. Parámetros escalares: paso por referencia mediante punteros . . . . . . . . 159
3.5.6. Paso de registros a funciones . . . . . . . . . . . . . . . . . . . . . . . . . 164
3.5.7. Paso de matrices y otros vectores multidimensionales . . . . . . . . . . . . 167
3.5.8. Tipos de retorno válidos . . . . . . . . . . . . . . . . . . . . . . . . . . . . 171
3.5.9. Un ejercicio práctico: miniGalaxis . . . . . . . . . . . . . . . . . . . . . . 171
3.6. Recursión . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 188
3.6.1. Un método recursivo de ordenación: mergesort . . . . . . . . . . . . . . . 189
3.6.2. Recursión indirecta y declaración anticipada . . . . . . . . . . . . . . . . . 195
3.7. Macros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 196
3.8. Otras cuestiones acerca de las funciones . . . . . . . . . . . . . . . . . . . . . . . 199
3.8.1. Funciones inline . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 199
3.8.2. Variables locales static . . . . . . . . . . . . . . . . . . . . . . . . . . . . 200
3.8.3. Paso de funciones como parámetros . . . . . . . . . . . . . . . . . . . . . 201
3.9. Módulos, bibliotecas y unidades de compilación . . . . . . . . . . . . . . . . . . . 203
3.9.1. Declaración de prototipos en cabeceras . . . . . . . . . . . . . . . . . . . . 205
3.9.2. Declaración de variables en cabeceras . . . . . . . . . . . . . . . . . . . . 207
3.9.3. Declaración de registros en cabeceras . . . . . . . . . . . . . . . . . . . . . 208
5. Ficheros 319
5.1. Ficheros de texto y ficheros binarios . . . . . . . . . . . . . . . . . . . . . . . . . 319
5.1.1. Representación de la información en los ficheros de texto . . . . . . . . . . 319
5.1.2. Representación de la información en los ficheros binarios . . . . . . . . . . 320
5.2. Ficheros de texto . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 321
5.2.1. Abrir, leer/escribir, cerrar . . . . . . . . . . . . . . . . . . . . . . . . . . . 321
5.2.2. Aplicaciones: una agenda y un gestor de una colección de discos compactos328
5.2.3. Los ((ficheros)) de consola . . . . . . . . . . . . . . . . . . . . . . . . . . . 337
Introducción a C
RODN G Ó IREJ
Durante un rato, estuvo contemplando esto perpleja; pero al final se le ocurrió una
brillante idea. ¡Ah, ya sé!, ¡es un libro del Espejo, naturalmente! Si lo pongo delante de
un espejo, las palabras se verán otra vez del derecho.
El lenguaje de programación C es uno de los más utilizados (si no el que más) en la programación
de sistemas software. Es similar a Python en muchos aspectos fundamentales: presenta las
mismas estructuras de control (selección condicional, iteración), permite trabajar con algunos
tipos de datos similares (enteros, flotantes, secuencias), hace posible definir y usar funciones,
etc. No obstante, en muchas otras cuestiones es un lenguaje muy diferente.
C presenta ciertas caracterı́sticas que permiten ejercer un elevado control sobre la eficiencia
de los programas, tanto en la velocidad de ejecución como en el consumo de memoria, pero
a un precio: tenemos que proporcionar información explı́cita sobre gran cantidad de detalles,
por lo que generalmente resultan programas más largos y complicados que sus equivalentes en
Python, aumentando ası́ la probabilidad de que cometamos errores.
En este capı́tulo aprenderemos a realizar programas en C del mismo ((nivel)) que los que
sabı́amos escribir en Python tras estudiar el capı́tulo 4 del primer volumen. Aprenderemos,
pues, a usar variables, expresiones, la entrada/salida, funciones definidas en ((módulos)) (que
en C se denominan bibliotecas) y estructuras de control. Lo único que dejamos pendiente de
momento es el tratamiento de cadenas en C, que es sensiblemente diferente al que proporciona
Python.
Nada mejor que un ejemplo de programa en los dos lenguajes para que te lleves una primera
impresión de cuán diferentes son Python y C. . . y cuán semejantes. Estos dos programas, el
primero en Python y el segundo en C, calculan el valor de
b
X √
i
i=a
sumatorio.py sumatorio.py
1 from math import *
2
9 b = int(raw_input(’Lı́mite superior:’))
10 while b < a:
11 print ’No puede ser menor que %d’ % a
12 b = int(raw_input(’Lı́mite superior:’))
13
19 # Mostrar el resultado.
20 print ’Sumatorio de raı́ces’,
21 print ’de %d a %d: %f’ % (a, b, s)
sumatorio.c sumatorio.c
1 #include <stdio.h>
2 #include <math.h>
3
4 int main(void)
5 {
6 int a, b, i;
7 float s;
8
32 /* Mostrar el resultado. */
33 printf ("Sumatorio de raı́ces ");
34 printf ("de %d a %d: %f\n", a, b, s);
35
36 return 0;
37 }
En varios puntos de este capı́tulo haremos referencia a estos dos programas. No los pierdas
de vista.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 1 Compara los programas sumatorio.py y sumatorio.c. Analiza sus semejanzas y diferen-
cias. ¿Qué función desempeñan las llaves en sumatorio.c? ¿Qué función crees que desempeñan
las lı́neas 6 y 7 del programa C? ¿A qué elemento de Python se parecen las dos primeras lı́neas
de sumatorio.c? ¿Qué similitudes y diferencias aprecias entre las estructuras de control de
Python y C? ¿Cómo crees que se interpreta el bucle for del programa C? ¿Por qué algunas
lı́neas de sumatorio.c finalizan en punto y coma y otras no? ¿Qué diferencias ves entre los
comentarios Python y los comentarios C?
.............................................................................................
Un poco de historia
C ya tiene sus añitos. El nacimiento de C está estrechamente vinculado al del sistema opera-
tivo Unix. El investigador Ken Thompson, de AT&T, la compañı́a telefónica estadounidense,
se propuso diseñar un nuevo sistema operativo a principios de los setenta. Disponı́a de un
PDP-7 en el que codificó una primera versión de Unix en lenguaje ensamblador. Pronto se
impuso la conveniencia de desarrollar el sistema en un lenguaje de programación de alto
nivel, pero la escasa memoria del PDP-7 (8K de 18 bits) hizo que ideara el lenguaje de
programación B, una versión reducida de un lenguaje ya existente: BCPL. El lenguaje C
apareció como un B mejorado, fruto de las demandas impuestas por el desarrollo de Unix.
Dennis Ritchie fue el encargado del diseño del lenguaje C y de la implementación de un
compilador para él sobre un PDP-11.
C ha sufrido numerosos cambios a lo largo de su historia. La primera versión ((estable))
del lenguaje data de 1978 y se conoce como ((K&R C)), es decir, ((C de Kernighan y Ritchie)).
Esta versión fue descrita por sus autores en la primera edición del libro ((The C Programming
Language)) (un auténtico ((best-seller)) de la informática). La adopción de Unix como siste-
ma operativo de referencia en las universidades en los años 80 popularizó enormemente el
lenguaje de programación C. No obstante, C era atractivo por sı́ mismo y parecı́a satisfacer
una demanda real de los programadores: disponer de un lenguaje de alto nivel con ciertas
caracterı́sticas propias de los lenguajes de bajo nivel (de ahı́ que a veces se diga que C es
un lenguaje de nivel intermedio).
La experiencia con lenguajes de programación diseñados con anterioridad, como Lisp o
Pascal, demuestra que cuando el uso de un lenguaje se extiende es muy probable que proli-
feren variedades dialectales y extensiones para aplicaciones muy concretas, lo que dificulta
enormemente el intercambio de programas entre diferentes grupos de programadores. Para
evitar este problema se suele recurrir a la creación de un comité de expertos que define la
versión oficial del lenguaje. El comité ANSI X3J9 (ANSI son las siglas del American National
Standards Institute), creado en 1983, considera la inclusión de aquellas extensiones y mejo-
ras que juzga de suficiente interés para la comunidad de programadores. El 14 de diciembre
de 1989 se acordó qué era el ((C estándar)) y se publicó el documento con la especificación
en la primavera de 1990. El estándar se divulgó con la segunda edición de ((The C Pro-
gramming Language)), de Brian Kernighan y Dennis Ritchie. Un comité de la International
Standards Office (ISO) ratificó el documento del comité ANSI en 1992, convirtiéndolo ası́
en un estándar internacional. Durante mucho tiempo se conoció a esta versión del lenguaje
como ANSI-C para distinguirla ası́ del K&R C. Ahora se prefiere denominar a esta variante
C89 (o C90) para distinguirla de la revisión que se publicó en 1999, la que se conoce por
C99 y que es la versión estándar de C que estudiaremos.
C ha tenido un gran impacto en el diseño de otros muchos lenguajes. Ha sido, por
ejemplo, la base para definir la sintaxis y ciertos aspectos de la semántica de lenguajes tan
populares como Java y C++.
sumatorio Resultados
sumatorio Resultados
La opción -lm se debe usar siempre que nuestro programa utilice funciones del módulo
matemático (como sqrt, que se usa en sumatorio.c). Ya te indicaremos por qué en la sección
dedicada a presentar el módulo matemático de C.
1 Por razones de seguridad es probable que no baste con escribir sumatorio para poder ejecutar un programa
con ese nombre y que reside en el directorio activo. Si es ası́, prueba con ./sumatorio.
2 La versión 3.2 de gcc es la primera en ofrecer un soporte suficiente de C99. Si usas una versión anterior, es
C99 y gcc
Por defecto, gcc acepta programas escritos en C89 con extensiones introducidas por GNU
(el grupo de desarrolladores de muchas herramientas de Linux). Muchas de esas extensiones
de GNU forman ya parte de C99, ası́ que gcc es, por defecto, el compilador de un lenguaje
intermedio entre C89 y C99. Si en algún momento da un aviso indicando que no puede
compilar algún programa porque usa caracterı́sticas propias del C99 no disponibles por
defecto, puedes forzarle a compilar en ((modo C99)) ası́:
gcc programa.c -std=c99 -o programa
Has de saber, no obstante, que gcc aún no soporta el 100% de C99 (aunque sı́ todo lo
que te explicamos en este texto).
El compilador gcc acepta muchas otras variantes de C. Puedes forzarle a aceptar una
en particular ((asignando)) a la opción -std el valor c89, c99, gnu89 o gnu99.
1 #include <stdio.h>
2
5 int main(void)
6 {
7 Programa principal.
8
9 return 0;
10 }
Hay, pues, dos zonas: una inicial cuyas lı́neas empiezan por #include (equivalentes a las
sentencias import de Python) y una segunda que empieza con una lı́nea ((int main(void)))
y comprende las sentencias del programa principal mas una lı́nea ((return 0;)), encerradas
todas ellas entre llaves ({ y }).
De ahora en adelante, todo texto comprendido entre llaves recibirá el nombre de bloque.
2. Toda variable debe declararse antes de ser usada. La declaración de la variable consiste
en escribir el nombre de su tipo (int para enteros y float para flotantes)3 seguida del
identificador de la variable y un punto y coma. Por ejemplo, si vamos a usar una variable
entera con identificador a y una variable flotante con identificador b, nuestro programa
las declarará ası́:
1 #include <stdio.h>
2
3 int main(void)
4 {
5 int a;
6 float b;
7
10 return 0;
11 }
3 Recuerda que no estudiaremos las variables de tipo cadena hasta el próximo capı́tulo.
No es obligatorio que la declaración de las variables tenga lugar justo al principio del
bloque que hay debajo de la lı́nea ((int main(void))), pero sı́ conveniente.4
Si tenemos que declarar dos o más variables del mismo tipo, podemos hacerlo en una
misma lı́nea separando los identificadores con comas. Por ejemplo, si las variables x, y y
z son todas de tipo float, podemos recurrir a esta forma compacta de declaración:
1 #include <stdio.h>
2
3 int main(void)
4 {
5 float x, y, z;
6
7 ...
8
9 return 0;
10 }
3 int main(void)
4 {
5 int a;
6 float b;
7
8 a = 2;
9 b = 0.2;
10
11 return 0;
12 }
Como puedes ver, los números enteros y flotantes se representan igual que en Python.
4. Las expresiones se forman con los mismos operadores que aprendimos en Python. Bueno,
hay un par de diferencias:
Los operadores Python and, or y not se escriben en C, respectivamente, con &&, ||
y !;
No hay operador de exponenciación (que en Python era **).
Hay operadores para la conversión de tipos. Si en Python escribı́amos float(x) para
convertir el valor de x a flotante, en C escribiremos (float) x para expresar lo mismo.
Fı́jate en cómo se disponen los paréntesis: los operadores de conversión de tipos son
de la forma (tipo).
1 #include <stdio.h>
2
3 int main(void)
4 {
5 int a;
6 float b;
7
8 a = 13 % 2 ;
9 b = 2.0 / (1.0 + 2 - (a + 1)) ;
10
11 return 0;
12 }
4 En versiones de C anteriores a C99 sı́ era obligatorio que las declaraciones se hicieran al principio de un
bloque. C99 permite declarar una variable en cualquier punto del programa, siempre que éste sea anterior al
primer uso de la misma.
Las reglas de asociatividad y precedencia de los operadores son casi las mismas que apren-
dimos en Python. Hay más operadores en C y los estudiaremos más adelante.
5. Para mostrar resultados por pantalla se usa la función printf . La función recibe uno o
más argumentos separados por comas:
primero, una cadena con formato, es decir, con marcas de la forma %d para re-
presentar enteros y marcas %f para representar flotantes (en los que podemos usar
modificadores para, por ejemplo, controlar la cantidad de espacios que ocupará el
valor o la cantidad de cifras decimales de un número flotante);
y, a continuación, las expresiones cuyos valores se desea mostrar (debe haber una
expresión por cada marca de formato).
escribe.c escribe.c
1 #include <stdio.h>
2
3 int main(void)
4 {
5 int a;
6 float b;
7
8 a = 13 % 2;
9 b = 2.0 / (1.0 + 2 - (a + 1));
10
13 return 0;
14 }
La cadena con formato debe ir encerrada entre comillas dobles, no simples. El carácter
de retorno de carro (\n) es obligatorio si se desea finalizar la impresión con un salto de
lı́nea. (Observa que, a diferencia de Python, no hay operador de formato entre la cadena
de formato y las expresiones: la cadena de formato se separa de la primera expresión con
una simple coma).
Como puedes ver, todas las sentencias de los programas C que estamos presentando fina-
lizan con punto y coma.
6. Para leer datos de teclado has de usar la función scanf . Fı́jate en este ejemplo:
3 int main(void)
4 {
5 int a;
6 float b;
7
13 return 0;
14 }
3 int main(void)
4 {
5 int a;
6 float b;
7
15 return 0;
16 }
si es par.c si es par.c
1 #include <stdio.h>
2
3 int main(void)
4 {
5 int a;
6
10 if (a % 2 == 0) {
11 printf ("El valor de a es par.\n");
12 printf ("Es curioso.\n");
13 }
14
15 return 0;
16 }
3 int main(void)
4 {
5 int a;
6
10 if (a % 2 == 0) {
11 printf ("El valor de a es par.\n");
12 if (a > 0) {
13 printf ("Y, además, es positivo.\n");
14 }
15 }
16
17 return 0;
18 }
3 int main(void)
4 {
5 int a;
6
10 if (a % 2 == 0) {
11 printf ("El valor de a es par.\n");
12 }
13 else {
14 printf ("El valor de a es impar.\n");
15 }
16
17 return 0;
18 }
No hay, sin embargo, sentencia if -elif , aunque es fácil obtener el mismo efecto con una
sucesión de if -else if :
3 int main(void)
4 {
5 int a;
6
10 if (a > 0) {
11 printf ("El valor de a es positivo.\n");
12 }
13 else if (a == 0) {
14 printf ("El valor de a es nulo.\n");
15 }
16 else if (a < 0) {
17 printf ("El valor de a es negativo.\n");
18 }
19 else {
20 printf ("Es imposible mostrar este mensaje.\n");
21 }
22
23 return 0;
24 }
3 int main(void)
4 {
5 int a;
6
10 while (a > 0) {
11 printf ("%d", a);
12 a -= 1;
13 }
!
14 printf (" Boom!\n");
15
16 return 0;
17 }
primo.c primo.c
1 #include <stdio.h>
2
3 int main(void)
4 {
5 int a, b;
6
10 b = 2;
11 while (b < a) {
12 if (a % b == 0) {
13 break;
14 }
15 b += 1;
16 }
17 if (b == a) {
18 printf ("%d es primo.\n", a);
19 }
20 else {
21 printf ("%d no es primo.\n", a);
22 }
23
24 return 0;
25 }
10. Los módulos C reciben el nombre de bibliotecas y se importan con la sentencia #include.
Ya hemos usado #include en la primera lı́nea de todos nuestros programas: #include
<stdio.h>. Gracias a ella hemos importado las funciones de entrada/salida scanf y printf .
No se puede importar una sola función de una biblioteca: debes importar el contenido
completo de la biblioteca.
Las funciones matemáticas pueden importarse del módulo matemático con #include
<math.h> y sus nombres son los mismos que vimos en Python (sin para el seno, cos
para el coseno, etc.).
4 int main(void)
5 {
6 float b;
7
10
11 if (b >= 0.0) {
12 printf ("Su raı́z cuadrada es %f.\n", sqrt(b) );
13 }
14 else {
15 printf ("No puedo calcular su raı́z cuadrada.\n");
16 }
17
18 return 0;
19 }
No está mal: ya sabes traducir programas Python sencillos a C (aunque no sabemos traducir
programas con definiciones de función, ni con variables de tipo cadena, ni con listas, ni con
registros, ni con acceso a ficheros. . . ). ¿Qué tal practicar con unos pocos ejercicios?
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 2 Traduce a C este programa Python.
1 a = int(raw_input(’Dame el primer número: ’))
2 b = int(raw_input(’Dame el segundo número: ’))
3
4 if a >= b:
5 maximo = a
6 else:
7 maximo = b
8
4 if n * m == 100:
5 print ’El producto %d * %d es igual a 100’ % (n, m)
6 else:
7 print ’El producto %d * %d es distinto de 100’ % (n, m)
4 if a != 0:
5 x = -b/a
6 print ’Solución: ’, x
7 else:
8 if b != 0:
9 print ’La ecuación no tiene solución.’
10 else:
11 print ’La ecuación tiene infinitas soluciones.’
3 x = 1.0
4 while x < 10.0:
5 print x, ’\t’, log(x)
6 x = x + 1.0
3 opcion = 0
4 while opcion != 4:
5 print ’Escoge una opción: ’
6 print ’1) Calcular el diámetro.’
7 print ’2) Calcular el perı́metro.’
8 print ’3) Calcular el área.’
9 print ’4) Salir.’
10 opcion = int(raw_input(’Teclea 1, 2, 3 o 4 y pulsa el retorno de carro: ’))
11
14 if opcion == 1:
15 diametro = 2 * radio
16 print ’El diámetro es’, diametro
17 elif opcion == 2:
18 perimetro = 2 * pi * radio
19 print ’El perı́metro es’, perimetro
20 elif opcion == 3:
21 area = pi * radio ** 2
22 print ’El área es’, area
23 elif opcion < 0 or opcion > 4:
24 print ’Sólo hay cuatro opciones: 1, 2, 3 o 4. Tú has tecleado’, opcion
.............................................................................................
Ya es hora, pues, de empezar con los detalles de C.
al final (tı́picamente el valor 0), por lo que finaliza con una sentencia return que devuelve el
valor 0.5
La estructura tı́pica de un programa C es ésta:
Definición de funciones.
int main(void)
{
Declaración de variables propias del programa principal (o sea, locales a main).
Programa principal.
return 0;
}
Un fichero con extensión ((.c)) que no define la función main no es un programa C completo.
Si, por ejemplo, tratamos de compilar este programa incorrecto (no define main):
E sin main.c E
1 int a;
2 a = 1;
el compilador muestra el siguiente mensaje (u otro similar, según la versión del compilador que
utilices):
$ gcc sin_main.c -o sin_main
sin_main.c:2: warning: data definition has no type or storage class
/usr/lib/crt1.o: En la función ‘_start’:
/usr/lib/crt1.o(.text+0x18): referencia a ‘main’ sin definir
collect2: ld returned 1 exit status
Fı́jate en la tercera lı́nea del mensaje de error: ((referencia a ‘main’ sin definir)).
3 int main(void)
4 {
5 int a, b, c, minimo;
6
operativo Unix recibe el valor devuelto con el return y el intérprete de órdenes, por ejemplo, puede tomar una
decisión acerca de qué hacer a continuación en función del valor devuelto.
10 if (a < b) {
11 if (a < c) {
12 minimo = a;
13 }
14 else {
15 minimo = c;
16 }
17 }
18 else {
19 if (b < c) {
20 minimo = b;
21 }
22 else {
23 minimo = c;
24 }
25 }
26 printf ("%d\n", minimo);
27 return 0;
28 }
Este programa podrı́a haberse escrito como sigue y serı́a igualmente correcto:
3 int main(void)
4 {
5 int a, b, c, minimo;
6
Cuando un bloque consta de una sola sentencia no es necesario encerrarla entre llaves. Aquı́
tienes un ejemplo:
3 int main(void)
4 {
5 int a, b, c, minimo;
6
17 }
18 printf ("%d\n", minimo);
19 return 0;
20 }
De hecho, como if -else es una única sentencia, también podemos suprimir las llaves restantes:
minimo 3.c minimo.c
1 #include <stdio.h>
2
3 int main(void)
4 {
5 int a, b, c, minimo;
6
Debes tener cuidado, no obstante, con las ambigüedades que parece producir un sólo else y
dos if :
primero es minimo 1.c primero es minimo.c
1 #include <stdio.h>
2
3 int main(void)
4 {
5 int a, b, c, minimo;
6
¿Cuál de los dos if se asocia al else? C usa una regla: el else se asocia al if más próximo (en el
ejemplo, el segundo). No obstante, puede resultar más legible que explicites con llaves el alcance
de cada if :
primero es minimo 2.c primero es minimo.c
1 #include <stdio.h>
2
3 int main(void)
4 {
5 int a, b, c, minimo;
6
Ahora que has adquirido la práctica de indentar los programas gracias a la disciplina im-
puesta por Python, sı́guela siempre, aunque programes en C y no sea necesario.
d) Ídem, pero las dos llaves se disponen más a la derecha y el contenido del bloque más a
la derecha:
if (a==1)
{
b = 1;
c = 2;
}
e) Y aún otro, con las llaves a la misma altura que el contenido del bloque:
if (a==1)
{
b = 1;
c = 2;
}
No hay un estilo mejor que otro. Es cuestión de puro convenio. Aún ası́, hay más de
una discusión subida de tono en los grupos de debate para desarrolladores de C. Increı́ble,
¿no? En este texto hemos optado por el primer estilo de la lista (que, naturalmente, es el
((correcto)) ;-)) para todas las construcciones del lenguaje a excepción de la definición de
funciones (como main), que sigue el convenio de indentación que relacionamos en tercer
lugar.
Una norma: las sentencias C acaban con un punto y coma. Y una excepción a la norma: no
4 int main(void)
5 {
6 int a, b, i;
7 float s;
8
24 /* Mostrar el resultado. */
25 printf ("Sumatorio de raı́ces de %d a %d: %f\n", a, b, s);
26
27 return 0;
28 }
6 Habrá una excepción a esta norma: las construcciones struct, cuya llave de cierre debe ir seguida de un
punto y coma.
7 Quizá hayas reparado en que las lı́neas que empiezan con #include son especiales y que las tratamos de
forma diferente: no se puede jugar con su formato del mismo modo que con las demás: cada sentencia #include
debe ocupar una lı́nea y el carácter # debe ser el primero de la lı́nea.
long a
[4],b[
4],c[4]
,d[0400],e=1;
typedef struct f{long g
,h,i[4] ,j;struct f*k;}f;f g,*
l[4096 ]; char h[256],*m,k=3;
long n (o, p,q)long*o,*p,*q;{
long r =4,s,i=0;for(;r--;s=i^
*o^*p, i=i&*p|(i|*p)&~*o++,*q
++=s,p ++);return i;}t(i,p)long*p
;{*c=d [i],n(a,c,b),n(p,b,p);}u(j)f*j;{j->h
=(j->g =j->i[0]|j->i[1]|j->i[2]|j->i[3])&4095;}v(
j,s)f* j; {int i; for(j->k->k&&v(j->k, ’ ’),fseek(
stdin, j->j, 0);i=getchar(),putchar(i-’\n’?i:s),i-
’\n’;);}w(o,r,j,x,p)f*o,*j;long p;{f q;int
s,i=o->h;q.k=o;r>i?j=l[r=i]:r<i&&
(s=r&~i)?(s|=s>>1, s|=s
>>2,s|=s>>4,s
|=s>>8
,j=l[r
=((r&i |s)&~(s>>1))-1&i]):0;--x;for
(;x&&!(p&i);p>>=1);for(;!x&&j;n(o->i,j->i,q.
i),u(&q),q.g||(q.j=j->j,v(&q,’\n’)),j=j->k);for(;x;j=x
?j->k:0){for(;!j&&((r=(r&i)-1&i)-i&&(r&p)?2:(x=0));j=l[r]);!
x||(j->g&~o->g)||n (o->i,j->i,q.i)||(
u(&q), q.j=j ->j,q.g?w(&q
,r,j->k,x ,p):v(&q,
’\n’)); }}y(){f
j;char *z,*p;
for(;m ? j.j=
ftell( stdin)
,7,(m= gets(m ))||w(
&g,315 *13,l[ 4095]
,k,64* 64)&0: 0;n(g
.i,j.i, b)||(u (&j),j.
k=l[j.h],l[j.h]= &j,y())){for(z= p=h;*z&&(
d[*z++]||(p=0)););for(z=p?n(j.i ,j.i,j.i)+h:"";
*z;t(*z++,j.i));}}main(o,p)char** p; {for(;m = *++p;)for(;*m-
’-’?*m:(k= -atoi(m))&0;d[*m]||(d[*m ]=e,e<<=1),t(*m++,g.i)); u(&
g),m=h
,y();}
Los lenguajes de programación en los que el código no debe seguir un formato determinado
de lı́neas y/o bloques se denominan de formato libre. Python no es un lenguaje de formato libre;
C sı́.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 9 Este programa C incorrecto tiene varios errores que ya puedes detectar. Indica cuáles son:
1 #include <stdio.h>
3 int a, b;
4
maximo.c maximo.c
1 /*********************************************************************
2 * Un programa de ejemplo.
3 *--------------------------------------------------------------------
4 * Propósito: mostrar algunos efectos que se pueden lograr con
5 * comentarios de C
6 *********************************************************************/
8 #include <stdio.h>
9
10 /*---------------------------------------------------------------------
11 * Programa principal
12 *-------------------------------------------------------------------*/
14
15 int main(void)
16 {
17 int a, b, c; // Los tres números.
18 int m; // Variable para el máximo de los tres.
19
20 /* Lectura de un número */
21 printf ("a: "); scanf ("%d", &a);
22 /* ... de otro ... */
23 printf ("b: "); scanf ("%d", &b);
24 /* ... y de otro más. */
25 printf ("c: "); scanf ("%d", &c);
26 if (a > b)
27 if (a > c) //En este caso a > b y a > c.
28 m = a;
29 else //Y en este otro caso b < a ≤ c.
30 m = c;
31 else
32 if (b > c) //En este caso a ≤ b y b > c.
33 m = b;
34 else //Y en este otro caso a ≤ b ≤ c.
35 m = c;
36 /* Impresión del resultado. */))
37 printf ("El máximo de %d, %d y %d es %d\n", a, b, c, m);
38 return 0;
39 }
3 int main(void)
4 {
5 int a, b, i, j;
6
3 int main(void)
4 {
5 int a, b, i, j;
6
15 i += 1;
16 }
17 */
19 printf ("%d\n", j);
20 return 0;
21 }
3 int main(void)
4 {
5 int a, b, i, j;
6
.............................................................................................
1.6.1. Enteros
Una forma natural de expresar un número entero en C es mediante una secuencias de dı́gitos.
Por ejemplo, 45, 0 o 124653 son enteros. Al igual que en Python, está prohibido insertar espacios
en blanco (o cualquier otro sı́mbolo) entre los dı́gitos de un literal entero.
Hay más formas de expresar enteros. En ciertas aplicaciones resulta útil expresar un número
entero en base 8 (sistema octal) o en base 16 (sistema hexadecimal). Si una secuencia de dı́gitos
empieza en 0, se entiende que codifica un número en base 8. Por ejemplo, 010 es el entero 8 (en
base 10) y 0277 es el entero 191 (en base 10). Para codificar un número en base 16 debes usar
el par de caracteres 0x seguido del número en cuestión. El literal 0xff, por ejemplo, codifica el
valor decimal 255.
Pero aún hay una forma más de codificar un entero, una que puede resultar extraña al
principio: mediante un carácter entre comillas simples, que representa a su valor ASCII. El
valor ASCII de la letra ((a minúscula)), por ejemplo, es 97, ası́ que el literal ’a’ es el valor 97.
Hasta tal punto es ası́ que podemos escribir expresiones como ’a’+1, que es el valor 98 o, lo
que es lo mismo, ’b’.
Se puede utilizar cualquiera de las secuencias de escape que podemos usar con las cadenas.
El literal ’\n’, por ejemplo, es el valor 10 (que es el código ASCII del salto de lı́nea).
Ni ord ni chr
En C no son necesarias las funciones ord o chr de Python, que convertı́an caracteres en
enteros y enteros en caracteres. Como en C los caracteres son enteros, no resulta necesario
efectuar conversión alguna.
1.6.2. Flotantes
Los números en coma flotante siguen la misma sintaxis que los flotantes de Python. Un número
flotante debe presentar parte decimal y/o exponente. Por ejemplo, 20.0 es un flotante porque
tiene parte decimal (aunque sea nula) y 2e1 también lo es, pero porque tiene exponente (es decir,
tiene una letra e seguida de un entero). Ambos representan al número real 20.0. (Recuerda que
2e1 es 2 · 101 .) Es posible combinar en un número parte decimal con exponente: 2.0e1 es un
número en coma flotante válido.
1.6.3. Cadenas
Ası́ como en Python puedes optar por encerrar una cadena entre comillas simples o dobles, en
C sólo puedes encerrarla entre comillas dobles. Dentro de las cadenas puedes utilizar secuencias
de escape para representar caracteres especiales. Afortunadamente, las secuencias de escape son
las mismas que estudiamos en Python. Por ejemplo, el salto de lı́nea es \n y la comilla doble
es \"·.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 14 Traduce a cadenas C las siguientes cadenas Python:
1. "una cadena"
2. ’una cadena’
3. "una \"cadena\""
4. ’una "cadena"’
5. ’una \’cadena\’’
6. "una cadena que ocupa\n dos lı́neas"
7. "una cadena que \\no ocupa dos lı́neas"
.............................................................................................
Te relacionamos las secuencias de escape que puedes necesitar más frecuentemente:
Secuencia Valor
\a (alerta): produce un aviso audible o visible.
\b (backspace, espacio atrás): el cursor retrocede un espacio a la izquierda.
\f (form feed, alimentación de página): pasa a una nueva ((página)).
\n (newline, nueva lı́nea): el cursor pasa a la primera posición de la siguiente
lı́nea.
\r (carriage return, retorno de carro): el cursor pasa a la primera posición
de la lı́nea actual.
\t (tabulador): desplaza el cursor a la siguiente marca de tabulación.
\\ muestra la barra invertida.
\" muestra la comilla doble.
\número octal muestra el carácter cuyo código ASCII (o IsoLatin) es el número octal
indicado. El número octal puede tener uno, dos o tres dı́gitos octales.
Por ejemplo "\60" equivale a "0", pues el valor ASCII del carácter cero
es 48, que en octal es 60.
\xnúmero hexadecimal ı́dem, pero el número está codificado en base 16 y puede tener uno o
dos dı́gitos hexadecimales. Por ejemplo, "\x30" también equivale a "0",
pues 48 en decimal es 30 en hexadecimal.
\? muestra el interrogante.
Es pronto para aprender a utilizar variables de tipo cadena. Postergamos este asunto hasta
el apartado 2.2.
El tipo de datos int se usar normalmente para representar números enteros. La especificación
de C no define el rango de valores que podemos representar con una variable de tipo int, es
decir, no define el número de bits que ocupa una variable de tipo int. No obstante, lo más
frecuente es que ocupe 32 bits. Nosotros asumiremos en este texto que el tamaño de un entero
es de 32 bits, es decir, 4 bytes.
Como los enteros se codifican en complemento a 2, el rango de valores que podemos repre-
sentar es [−2147483648, 2147483647], es decir, [−231 , 231 − 1]. Este rango es suficiente para las
aplicaciones que presentaremos. Si resulta insuficiente o excesivo para alguno de tus programas,
consulta el catálogo de tipos que presentamos en el apéndice A.
En C, tradicionalmente, los valores enteros se han utilizado para codificar valores booleanos.
El valor 0 representa el valor lógico ((falso)) y cualquier otro valor representa ((cierto)). En la
última revisión de C se ha introducido un tipo booleano, aunque no lo usaremos en este texto
porque, de momento, no es frecuente encontrar programas que lo usen.
¿Para qué desperdiciar el bit más significativo en una variable entera de 32 bits que nunca
almacenará valores negativos? C te permite definir variables de tipo ((entero sin signo)). El tipo
tiene un nombre compuesto por dos palabras: ((unsigned int)) (aunque la palabra unsigned,
sin más, es sinónimo de unsigned int).
Gracias al aprovechamiento del bit extra es posible aumentar el rango de valores positivos
representables, que pasa a ser [0, 232 − 1], o sea, [0, 4294967295].
El tipo de datos float representa números en coma flotante de 32 bits. La codificación de coma
flotante permite definir valores con decimales. El máximo valor que puedes almacenar en una
variable de tipo float es 3.40282347 · 1038 . Recuerda que el factor exponencial se codifica en los
programas C con la letra ((e)) (o ((E))) seguida del exponente. Ese valor, pues, se codifica ası́ en
un programa C: 3.40282347e38. El número no nulo más pequeño (en valor absoluto) que puedes
almacenar en una variable float es 1.17549435 · 10−38 (o sea, el literal flotante 1.17549435e-38).
Da la impresión, pues, de que podemos representar números con 8 decimales. No es ası́: la
precisión no es la misma para todos los valores: es tanto mayor cuanto más próximo a cero es
el valor.
8 Bueno, esos son los que hemos estudiado. Python tiene, además, enteros largos. Otro tipo numérico no
00011110
+ 11100100
00000010
01111111
+ 00000001
10000000
Se puede declarar una serie de variables del mismo tipo en una sola sentencia de declaración
separando sus identificadores con comas. Este fragmento, por ejemplo, declara tres variables de
tipo entero y otras dos de tipo flotante.
int x, y, z;
float u, v;
redeclara.c E redeclara.c E
1 #include <stdio.h>
2
3 int main(void)
4 {
5 int a ;
6 float a ;
8 a = 2;
9 return 0;
10 }
El compilador nos indica que la variable a presenta un conflicto de tipos en la lı́nea 6 y que
ya habı́a sido declarada previamente en la lı́nea 5.
1 #include <stdio.h>
2
3 int main(void)
4 {
5 int a = 2;
6 float b = 2.0, c, d = 1.0, e;
7
8 return 0;
9 }
1 #include <stdio.h>
2
3 int main(void)
4 {
5 printf ("Una cadena");
6 printf ("y otra.");
7 return 0;
8 }
La función printf no añade un salto de lı́nea automáticamente, como sı́ hacı́a print en Python.
En el programa anterior, ambas cadenas se muestran una a continuación de otra. Si deseas que
haya un salto de lı́nea, deberás escribir \n al final de la cadena.
1 #include <stdio.h>
2
3 int main(void)
4 {
5 printf ("Una cadena\n");
6 printf ("y otra.\n");
7 return 0;
8 }
Tipo Marca
int %d
unsigned int %u
float %f
char %hhd
unsigned char %hhu
Por ejemplo, si a es una variable de tipo int con valor 5, b es una variable de tipo float con
valor 1.0, y c es una variable de tipo char con valor 100, esta llamada a la función printf :
printf ("Un entero: %d, un flotante: %f, un byte: %hhd\n", a, b, c);
¡Ojo! a la cadena de formato le sigue una coma, y no un operador de formato como sucedı́a
en Python. Cada variable se separa de las otras con una coma.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 15 ¿Que mostrará por pantalla esta llamada a printf suponiendo que a es de tipo entero y
vale 10?
printf ("%d-%d\n", a+1, 2+2);
.............................................................................................
Las marcas de formato para enteros aceptan modificadores, es decir, puedes alterar la repre-
sentación introduciendo ciertos caracteres entre el sı́mbolo de porcentaje y el resto de la marca.
Aquı́ tienes los principales:
Un número positivo: reserva un número de espacios determinado (el que se indique) para
representar el valor y muestra el entero alineado a la derecha.
Ejemplo: la sentencia
muestra en pantalla:
[ 10]
Un número negativo: reserva tantos espacios como indique el valor absoluto del número
para representar el entero y muestra el valor alineado a la izquierda.
Ejemplo: la sentencia
muestra en pantalla:
[10 ]
Un número que empieza por cero: reserva tantos espacios como indique el valor absoluto
del número para representar el entero y muestra el valor alineado a la izquierda. Los
espacios que no ocupa el entero se rellenan con ceros.
Ejemplo: la sentencia
muestra en pantalla:
[000010]
muestra en pantalla:
[ +10]
Hay dos notaciones alternativas para la representación de flotantes que podemos seleccionar
mediante la marca de formato adecuada:
Tipo Notación Marca
float Convencional %f
float Cientı́fica %e
La forma convencional muestra los números con una parte entera y una decimal separadas
por un punto. La notación cientı́fica representa al número como una cantidad con una sola
cifra entera y una parte decimal, pero seguida de la letra ((e)) y un valor entero. Por ejemplo, en
notación cientı́fica, el número 10.1 se representa con 1.010000e+01 y se interpreta ası́: 1.01×101 .
También puedes usar modificadores para controlar la representación en pantalla de los flotan-
tes. Los modificadores que hemos presentado para los enteros son válidos aquı́. Tienes, además,
la posibilidad de fijar la precisión:
Un punto seguido de un número: indica cuántos decimales se mostrarán.
Ejemplo: la sentencia
muestra en pantalla:
[ 10.10]
muestra en pantalla:
[a]
Recuerda que el valor 97 también puede representarse con el literal ’a’, ası́ que esta otra
sentencia
printf ("[%c]", ’a’);
Aún no sabemos almacenar cadenas en variables, ası́ que poca aplicación podemos encontrar
de momento a la marca %s. He aquı́, de todos modos, un ejemplo trivial de uso:
printf ("[%s]", "una cadena");
También puedes usar números positivos y negativos como modificadores de estas marcas.
Su efecto es reservar los espacios que indiques y alinear a derecha o izquierda.
Aquı́ tienes un programa de ejemplo en el que se utilizan diferentes marcas de formato con
y sin modificadores.
modificadores.c modificadores.c
1 #include <stdio.h>
2
3 int main(void)
4 {
5 char c = ’a’;
6 int i = 1000000;
7 float f = 2e1;
8
!
9 printf ("c : %c %hhd <- IMPORTANTE! Estudia la diferencia.\n", c, c);
10 printf ("i : %d |%10d|%-10d|\n", i, i, i);
11 printf ("f : %f |%10.2f|%+4.2f|\n", f , f , f );
12 return 0;
13 }
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 16 ¿Qué muestra por pantalla cada uno de estos programas?
a) ascii1.c ascii1.c
1 #include <stdio.h>
2
3 int main(void)
4 {
5 char i;
6 for (i=’A’; i<=’Z’; i++)
7 printf ("%c", i);
8 printf ("\n");
9 return 0;
10 }
b) ascii2.c ascii2.c
1 #include <stdio.h>
2
3 int main(void)
4 {
5 char i;
6 for (i=65; i<=90; i++)
7 printf ("%c", i);
8 printf ("\n");
9 return 0;
10 }
c) ascii3.c ascii3.c
1 #include <stdio.h>
2
3 int main(void)
4 {
5 int i;
6 for (i=’A’; i<=’Z’; i++)
7 printf ("%d ", i);
8 printf ("\n");
9 return 0;
10 }
d) ascii4.c ascii4.c
1 #include <stdio.h>
2
3 int main(void)
4 {
5 int i;
6 for (i=’A’; i<=’Z’; i++)
7 printf ("%d-%c ", i, i);
8 printf ("\n");
9 return 0;
10 }
e) ascii5.c ascii5.c
1 #include <stdio.h>
2
3 int main(void)
4 {
5 char i;
6 for (i=’A’; i<=’z’; i++) // Ojo: la z es minúscula.
7 printf ("%d ", (int) i);
8 printf ("\n");
9 return 0;
10 }
· 17 Diseña un programa que muestre la tabla ASCII desde su elemento de código numérico
32 hasta el de código numérico 126. En la tabla se mostrarán los códigos ASCII, además de
las respectivas representaciones como caracteres de sus elementos. Aquı́ tienes las primeras y
últimas lı́neas de la tabla que debes mostrar (debes hacer que tu programa muestre la informa-
ción exactamente como se muestra aquı́):
+---------+----------+
| Decimal | Carácter |
+---------+----------+
| 32 | |
| 33 | ! |
| 34 | " |
| 35 | # |
| 36 | $ |
| 37 | % |
... ...
| 124 | | |
| 125 | } |
| 126 | ~ |
+---------+----------+
.............................................................................................
Hay un rico juego de marcas de formato y las recogemos en el apéndice A. Consúltalo si
usas tipos diferentes de los que presentamos en el texto o si quieres mostrar valores enteros en
base 8 o 16. En cualquier caso, es probable que necesites conocer una marca especial, %%, que
sirve para mostrar el sı́mbolo de porcentaje. Por ejemplo, la sentencia
printf ("[%d%%]", 100);
muestra en pantalla:
[100%]
3 int main(void)
4 {
5 int a, b;
6
7 a = 0;
8 b = a + 8;
9
10 return 0;
11 }
reserva 8 bytes para albergar dos valores enteros.9 Imagina que a ocupa los bytes 1000–1003 y
b ocupa los bytes 1004–1007. Podemos representar la memoria ası́:
Observa que, inicialmente, cuando se reserva la memoria, ésta contiene un patrón de bits
arbitrario. La sentencia a = 0 se interpreta como ((almacena el valor 0 en la dirección de memoria
de a)), es decir, ((almacena el valor 0 en la dirección de memoria 1000))10 . Este es el resultado
de ejecutar esa sentencia:
9 En el apartado 3.5.2 veremos que la reserva se produce en una zona de memoria especial llamada pila. No
conviene que nos detengamos ahora a considerar los matices que ello introduce en el discurso.
10 En realidad, en la zona de memoria 1000–1003, pues se modifica el contenido de 4 bytes. En aras de la
brevedad, nos referiremos a los 4 bytes sólo con la dirección del primero de ellos.
Hemos supuesto que a está en la dirección 1000 y b en la 1004, pero ¿podemos saber en qué
direcciones de memoria se almacenan realmente a y b? Sı́: el operador & permite conocer la
dirección de memoria en la que se almacena una variable:
direcciones.c direcciones.c
1 #include <stdio.h>
2
3 int main(void)
4 {
5 int a, b;
6
7 a = 0;
8 b = a + 8;
9
13 return 0;
14 }
Observa que usamos la marca de formato %u para mostrar el valor de la dirección de memoria,
pues debe mostrarse como entero sin signo. La conversión a tipo unsigned int evita molestos
mensajes de aviso al compilar.11
Al ejecutar el programa tenemos en pantalla el siguiente texto (puede que si ejecutas tú
mismo el programa obtengas un resultado diferente):
Dirección de a: 3221222580
Dirección de b: 3221222576
O sea, que en realidad este otro gráfico representa mejor la disposición de las variables en
memoria:
11 Hay un marca especial, %p, que muestra directamente la dirección de memoria sin necesidad de efectuar la
a 0
b 8
Las direcciones de memoria de las variables se representarán con flechas que apuntan a sus
correspondientes cajas:
&a
a 0
&b
b 8
Ahora que hemos averiguado nuevas cosas acerca de las variables, vale la pena que reflexio-
nemos brevemente sobre el significado de los identificadores de variables allı́ donde aparecen.
Considera este sencillo programa:
1 #include <stdio.h>
2
3 int main(void)
4 {
5 int a, b;
6
7 a = 0;
8 b = a;
9 scanf ("%d", &b);
10 a = a + b;
11
12 return 0;
13 }
La función scanf necesita una dirección de memoria para saber dónde debe depositar un
resultado. Como no estamos en una sentencia de asignación, sino en una expresión, es necesario
que obtengamos explı́citamente la dirección de memoria con el operador &b. Ası́, para leer por
teclado el valor de b usamos la llamada scanf ("%d", &b).
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 18 Interpreta el significado de la sentencia a = a + b.
.............................................................................................
Observa que la variable cuyo valor se lee por teclado va obligatoriamente precedida por el
operador &: es ası́ como obtenemos la dirección de memoria en la que se almacena el valor de
la variable. Uno de los errores que cometerás con mayor frecuencia es omitir el carácter & que
debe preceder a todas las variables escalares en scanf .
Recuerda: la función scanf recibe estos datos:
Una cadena cuya marca de formato indica de qué tipo es el valor que vamos a leer por
teclado:
Tipo Marca
int %d
unsigned int %u
float %f
char como entero %hhd
char como carácter %c
unsigned char como entero %hhu
unsigned char como carácter %c
A scanf le estamos pasando la dirección de memoria de la variable c. Hasta ahı́, bien. Pero
c sólo ocupa un byte y a scanf le estamos diciendo que ((rellene)) 4 bytes con un número
entero a partir de esa dirección de memoria. Otro error de consecuencias gravı́simas. La
marca de formato adecuada para leer un número de tipo char hubiera sido %hhd.
1.12. Expresiones
Muchos de los sı́mbolos que representan a los operadores de Python que ya conoces son los mis-
mos en C. Los presentamos ahora agrupados por familias. (Consulta los niveles de precedencia
-Wall
Cuando escribimos un texto en castellano podemos cometer tres tipos de errores:
Errores léxicos: escribimos palabras incorrectamente, con errores ortográficos, o usa-
mos palabras inexistentes. Por ejemplo: ((herror)), ((lécsico)), ((jerigóndor)).
Errores sintácticos: aunque las palabras son válidas y están correctamente escritas,
faltan componentes de una frase (como el sujeto o el verbo), no hay concordancia
entre componentes de la frase, los componentes de la frase no ocupan la posición ade-
cuada, etc. Por ejemplo: ((el error sintáctica son)), ((la compilador detectó
errores)).
Errores semánticos: la frase está correctamente construida pero carece de significado
válido en el lenguaje. Por ejemplo: ((el compilador silbó una tonada en vı́deo)),
((los osos son enteros con decimales romos)).
Lo mismo ocurre con los programas C; pueden contener errores de los tres tipos:
Errores léxicos: usamos carácteres no válidos o construimos incorrectamente compo-
nentes elementales del programa (como identificadores, cadenas, palabras clave, etc.).
Por ejemplo: ((@3)), (("una cadena sin cerrar)).
Errores sintácticos: construı́mos mal una sentencia aunque usamos palabras válidas.
Por ejemplo: ((while a < 10 { a += 1; })), ((b = 2 * / 3;)).
Errores semánticos: la sentencia no tiene un significado ((válido)). Por ejemplo, si a
es de tipo float, estas sentencias contienen errores semánticos: ((scanf ("%d", &a);))
(se trata de leer el valor de a como si fuera un entero), ((if (a = 1.0) { a = 2.0; }))
(no se está comparando el valor de a con 1.0, sino que se asigna el valor 1.0 a a).
El compilador de C no deja pasar un solo error léxico o sintáctico: cuando lo detecta, nos
informa del error y no genera traducción a código de máquina del programa. Con los errores
semánticos, sin embargo, el compilador es más indulgente: la filosofı́a de C es suponer que
el programador puede tener una buena razón para hacer algunas de las cosas que expresa
en los programas, aunque no siempre tenga un significado ((correcto)) a primera vista. No
obstante, y para según qué posibles errores, el compilador puede emitir avisos (warnings).
Es posible regular hasta qué punto deseamos que el compilador nos proporcione avisos.
La opción -Wall (((Warning all)), que significa ((todos los avisos))) activa la detección de
posibles errores semánticos, notificándolos como avisos. Este programa erróneo, por ejemplo,
no genera ningún aviso al compilarse sin -Wall :
semanticos.c E semanticos.c E
1 #include <stdio.h>
2 int main(void)
3 {
4 float a;
5 scanf ("%d", &a);
6 if (a = 0.0) { a = 2.0; }
7 return 0;
8 }
y asociatividad en la tabla de la página 39.) Presta especial atención a los operadores que no
3 int main(void)
4 {
5 int a, b;
6 printf ("Introduce dos enteros: ");
7 scanf ("%d %d", &a, &b);
8 printf ("Valores leı́dos: %d y %d\n", a, b);
9 return 0;
10 }
También podemos especificar con cierto detalle cómo esperamos que el usuario introduzca
la información. Por ejemplo, con scanf ("%d-%d", &a, &b) indicamos que el usuario de-
be separar los enteros con un guión; y con scanf ("(%d,%d)", &a, &b) especificamos que
esperamos encontrar los enteros encerrados entre paréntesis y separados por comas.
Lee la página de manual de scanf (escribiendo man 3 scanf en el intérprete de órdenes
Unix) para obtener más información.
conoces por el lenguaje de programación Python, como son los operadores de bits, el operador
condicional o los de incremento/decremento.
Operadores aritméticos Suma (+), resta (-), producto (*), división (/), módulo o resto de
la división (%), identidad (+ unario), cambio de signo (- unario).
No hay operador de exponenciación.12
La división de dos números enteros proporciona un resultado de tipo entero (como ocurrı́a
en Python).
Los operadores aritméticos sólo funcionan con datos numéricos13 . No es posible, por ejem-
plo, concatenar cadenas con el operador + (cosa que sı́ podı́amos hacer en Python).
La dualidad carácter-entero del tipo char hace que puedas utilizar la suma o la resta
(o cualquier otro operador aritmético) con variables o valores de tipo char. Por ejemplo
’a’ + 1 es una expresión válida y su valor es ’b’ (o, equivalentemente, el valor 98, ya que
’a’ equivale a 97). (Recuerda, no obstante, que un carácter no es una cadena en C, ası́
que "a" + 1 no es "b".)
Operadores lógicos Negación o no-lógica (!), y-lógica o conjunción (&&) y o-lógica o disyun-
ción (||).
Los sı́mbolos son diferentes de los que aprendimos en Python. La negación era allı́ not,
la conjunción era and y la disyunción or.
C sigue el convenio de que 0 significa falso y cualquier otro valor significa cierto. Ası́ pues,
cualquier valor entero puede interpretarse como un valor lógico, igual que en Python.
Operadores de comparación Igual que (==), distinto de (!=), menor que (<), mayor que (>),
menor o igual que (<=), mayor o igual que (>=).
Son viejos conocidos. Una diferencia con respecto a Python: sólo puedes usarlos para
comparar valores escalares. No puedes, por ejemplo, comparar cadenas mediante estos
operadores.
La evaluación de una comparación proporciona un valor entero: 0 si el resultado es falso
y cualquier otro si el resultado es cierto (aunque normalmente el valor para cierto es 1).
12 Pero hay una función de la biblioteca matemática que permite calcular la potencia de un número: pow .
13 Yla suma y la resta trabajan también con punteros. Ya estudiaremos la denominada ((aritmética de punteros))
más adelante.
Operadores de bits Complemento (~), ((y)) (&), ((o)) (|), ((o)) exclusiva (^), desplazamiento a
izquierdas (<<), desplazamiento a derechas (>>).
Estos operadores trabajan directamente con los bits que codifican un valor entero. Aunque
también están disponibles en Python, no los estudiamos entonces porque son de uso
infrecuente en ese lenguaje de programación.
El operador de complemento es unario e invierte todos los bits del valor. Tanto & como |
y ^ son operadores binarios. El operador & devuelve un valor cuyo n-ésimo bit es 1 si y
sólo si los dos bits de la n-ésima posición de los operandos son también 1. El operador |
devuelve 0 en un bit si y solo si los correspondientes bits en los operandos son también
0. El operador ^ devuelve 1 si y sólo si los correspondientes bits en los operandos son
diferentes. Lo entenderás mejor con un ejemplo. Imagina que a y b son variables de tipo
char que valen 6 y 3, respectivamente. En binario, el valor de a se codifica como 00000110
y el valor de b como 00000011. El resultado de a | b es 7, que corresponde al valor en
base diez del número binario 000000111. El resultado de a & b es, en binario, 000000010,
es decir, el valor decimal 2. El resultado binario de a ^ b es 000000101, que en base 10
es 5. Finalmente, el resultado de ~a es 11111001, es decir, −7 (recuerda que un número
con signo está codificado en complemento a 2, ası́ que si su primer bit es 1, el número es
negativo).
Los operadores de desplazamiento desplazan los bits un número dado de posiciones a
izquierda o derecha. Por ejemplo, 16 como valor de tipo char es 00010000, ası́ que 16 << 1
es 32, que en binario es 00100000, y 16 >> 1 es 8, que en binario es 00001000.
Operadores de asignación Asignación (=), asignación con suma (+=), asignación con resta
(-=), asignación con producto (*=), asignación con división (/=), asignación con módulo
(%=), asignación con desplazamiento a izquierda (<<=), asignación con desplazamiento
a derecha (>>=), asignación con ((y)) (&=), asignación con ((o)) (|=), asignación con ((o))
exclusiva (^=).
Puede resultarte extraño que la asignación se considere también un operador. Que sea un
operador permite escribir asignaciones múltiples como ésta:
a = b = 1;
Es un operador asociativo por la derecha, ası́ que las asignaciones se ejecutan en este
orden:
a = (b = 1);
El valor que resulta de evaluar una asignación con = es el valor asignado a su parte
izquierda. Cuando se ejecuta b = 1, el valor asignado a b es 1, ası́ que ese valor es el que
se asigna también a a.
La asignación con una operación ((op)) hace que a la variable de la izquierda se le asigne
el resultado de operar con ((op)) su valor con el operando derecho. Por ejemplo, a /= 3 es
equivalente a a = a / 3.
Este tipo de asignación con operación recibe el nombre de asignación aumentada.
Operador de tamaño sizeof .
El operador sizeof puede aplicarse a un nombre de tipo (encerrado entre paréntesis) o
a un identificador de variable. En el primer caso devuelve el número de bytes que ocupa
en memoria una variable de ese tipo, y en el segundo, el número de bytes que ocupa esa
variable. Si a es una variable de tipo char, tanto sizeof (a) como sizeof (char) devuelven
el valor 1. Ojo: recuerda que ’a’ es literal entero, ası́ que sizeof (’a’) vale 4.
Operadores de coerción o conversión de tipos (en inglés ((type casting operator))). Pue-
des convertir un valor de un tipo de datos a otro que sea ((compatible)). Para ello dispones
de operadores de la forma (tipo), donde tipo es int, float, etc.
Por ejemplo, si deseas efectuar una división entre enteros que no pierda decimales al
convertir el resultado a un flotante, puedes hacerlo como te muestra este programa:
1 #include <stdio.h>
2
3 int main(void)
4 {
5 float x;
6 int a = 1, b = 2;
7
8 x = a / (float) b ;
9 }
if (x > 10)
a = 100;
else
a = 200;
devuelve el valor 0, se interpreta el resultado como ((falso)); en caso contrario, el resultado es ((cierto)).
La expresión i++ primero se evalúa como el valor actual de i y después hace que i
incremente su valor en una unidad.
La expresión ++i primero incrementa el valor de i en una unidad y después se evalúa
como el valor actual (que es el que resulta de efectuar el incremento).
Si el operador se está aplicando en una expresión, esta diferencia tiene importancia. Su-
pongamos que i vale 1 y que evaluamos esta asignación:
a = i++;
a = ++i;
C++
Ya debes entender de dónde viene el nombre C++: es un C ((incrementado)), o sea, mejorado.
En realidad C++ es mucho más que un C con algunas mejoras: es un lenguaje orientado a
objetos, ası́ que facilita el diseño de programas siguiendo una filosofı́a diferente de la propia
de los lenguajes imperativos y procedurales como C. Pero esa es otra historia.
En esta tabla te relacionamos todos los operadores (incluso los que aún no te hemos presen-
tado con detalle) ordenados por precedencia (de mayor a menor) y con su aridad (número de
operandos) y asociatividad:
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 19 Sean a, b y c tres variables de tipo int cuyos valores actuales son 0, 1 y 2, respectivamente.
¿Qué valor tiene cada variable tras ejecutar esta secuencia de asignaciones?
1 a = b++ - c--;
2 a += --b;
3 c *= a + b;
4 a = b | c;
5 b = (a > 0) ? ++a : ++c;
6 b <<= a = 2;
7 c >>= a == 2;
8 a += a = b + c;
ternario.c ternario.c
1 #include <stdio.h>
2
3 int main(void)
4 {
5 int a, b, c, r;
6
15 return 0;
16 }
· 21 Haz un programa que solicite el valor de x y muestre por pantalla el resultado de evaluar
x4 − x2 + 1. (Recuerda que en C no hay operador de exponenciación.)
· 22 Diseña un programa C que solicite la longitud del lado de un cuadrado y muestre por
pantalla su perı́metro y su área.
· 23 Diseña un programa C que solicite la longitud de los dos lados de un rectángulo y
muestre por pantalla su perı́metro y su área.
· 24 Este programa C es problemático:
un misterio.c un misterio.c
1 #include <stdio.h>
2
3 int main(void)
4 {
5 int a, b;
6
7 a = 2147483647;
8 b = a + a;
9 printf ("%d\n", a);
10 printf ("%d\n", b);
11 return 0;
12 }
¿Qué ha ocurrido?
· 25 Diseña un programa C que solicite el radio r de una circunferencia y muestre por
pantalla su perı́metro (2πr) y su área (πr2 ).
· 26 Si a es una variable de tipo char con el valor 127, ¿qué vale ~a? ¿Y qué vale !a? Y si a
es una variable de tipo unsigned int con el valor 2147483647, ¿qué vale ~a? ¿Y qué vale !a?
· 28 ¿Por qué si a es una variable entera a / 2 proporciona el mismo resultado que a >> 1?
¿Con qué operación de bits puedes calcular a * 2? ¿Y a / 32? ¿Y a * 128?
· 29 ¿Qué hace este programa?
swap.c swap.c
1 #include <stdio.h>
2
3 int main(void)
4 {
5 unsigned char a, b;
6 printf ("Introduce el valor de a (entre 0 y 255): "); scanf ("%hhu",&a);
7 printf ("Introduce el valor de b (entre 0 y 255): "); scanf ("%hhu",&b);
8
9 a ^= b;
10 b ^= a;
11 a ^= b;
12
16 return 0;
17 }
(Nota: la forma en que hace lo que hace viene de un viejo truco de la programación en
ensamblador,
. . . . . . . . . . . . . .donde
. . . . . . hay
. . . . .ricos
. . . . .juegos
. . . . . . .de
. . .instrucciones
. . . . . . . . . . . . .para
. . . . .la
. . .manipulación
. . . . . . . . . . . . . .de
. . datos
. . . . . . bit
. . . .a. .bit.)
....
¿5 > 3 > 2?
Recuerda que en Python podı́amos combinar operadores de comparación para formar ex-
presiones como 5 > 3 > 2. Esa, en particular, se evalúa a True, pues 5 es mayor que 3 y 3
es menor que 2. C también acepta esa expresión, pero con un significado completamente
diferente basado en la asociatividad por la izquierda del operador >: en primer lugar evalúa
la subexpresión 5 > 3, que proporciona el valor ((cierto)); pero como ((cierto)) es 1 (valor por
defecto) y 1 no es mayor que 2, el resultado de la evaluación es 0, o sea, ((falso)).
¡Ojo con la interferencia entre ambos lenguajes! Problemas como éste surgirán con fre-
cuencia cuando aprendas nuevos lenguajes: construcciones que significan algo en el lenguaje
que conoces bien tienen un significado diferente en el nuevo.
Si asignas a un entero int el valor de un entero más corto, como un char, el entero corto
promociona a un entero int automáticamente. Es decir, es posible efectuar esta asignación sin
riesgo alguno:
i = c;
Pero, ¿cómo? ¡En un byte (lo que ocupa un char) no caben cuatro (los que ocupa un int)! C
toma los 8 bits menos significativos de i y los almacena en c, sin más. La conversión funciona
correctamente, es decir, preserva el valor, sólo si el número almacenado en i está comprendido
entre −128 y 127.
Observa este programa:
conversion delicada.c conversion delicada.c
1 #include <stdio.h>
2
3 int main(void)
4 {
5 int a, b;
6 char c, d;
7
8 a = 512;
9 b = 127;
10 c = a;
11 d = b;
12 printf ("%hhd %hhd\n", c, d);
13
14 return 0;
15 }
¿Por qué el primer resultado es 0? El valor 512, almacenado en una variable de tipo int,
se representa con este patrón de bits: 00000000000000000000001000000000. Sus 8 bits menos
significativos se almacenan en la variable c al ejecutar la asignación c = a, es decir, c almacena
el patrón de bits 00000000, que es el valor decimal 0.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 30 ¿Qué mostrará por pantalla este programa?
otra conversion delicada.c otra conversion delicada.c
1 #include <stdio.h>
2
3 int main(void)
4 {
5 int a, b;
6 char c, d;
7 unsigned char e, f ;
8
9 a = 384;
10 b = 256;
11 c = a;
12 d = b;
13 e = a;
14 f = b;
15 printf ("%hhd %hhd\n", c, d);
16 printf ("%hhu %hhu\n", e, f );
17
18 return 0;
19 }
.............................................................................................
Si asignamos un entero a una variable flotante, el entero promociona a su valor equivalente
en coma flotante. Por ejemplo, esta asignación almacena en x el valor 2.0 (no el entero 2).
x = 2;
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 31 ¿Qué valor se almacena en las variables i (de tipo int) y x (de tipo float) tras ejecutar
cada una de estas sentencias?
a) i = 2; c) i = 2 / 4; e) x = 2.0 / 4.0; g) x = 2 / 4;
b) i = 1 / 2; d) i = 2.0 / 4; f) x = 2.0 / 4; h) x = 1 / 2;
.............................................................................................
Aunque C se encarga de efectuar implı́citamente muchas de las conversiones de tipo, pue-
de que en ocasiones necesites indicar explı́citamente una conversión de tipo. Para ello, debes
preceder el valor a convertir con el tipo de destino encerrado entre paréntesis. Ası́:
i = (int) 2.3;
En este ejemplo da igual poner (int) que no ponerlo: C hubiera hecho la conversión implı́ci-
tamente. El término (int) es el operador de conversión a enteros de tipo int. Hay un operador
de conversión para cada tipo: (char), (unsigned int) (float), etc. . . Recuerda que el sı́mbolo
(tipo) es un operador unario conocido como operador de coerción o conversión de tipos.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 32 ¿Qué valor se almacena en las variables i (de tipo int) y x (de tipo float) tras ejecutar
estas sentencias?
a) i = (float) 2; e) x = 2.0 / (int) 4.0; i) x = (float) (1 / 2);
b) i = 1 / (float) 2; f) x = (int) 2.0 / 4; j) x = 1 / (float) 2;
c) i = (int) (2 / 4); g) x = (int) (2.0 / 4);
d) i = (int) 2. / (float) 4; h) x = 2 / (float) 4;
.............................................................................................
1.15. Constantes
1.15.1. Definidas con la directiva define
Una diferencia de C con respecto a Python es la posibilidad que tiene el primero de definir
constantes. Una constante es, en principio15 , una variable cuyo valor no puede ser modificado.
Las constantes se definen con la directiva #define. Ası́:
#define PI 3.1415926535897931159979634685442
#define E 2.7182818284590450907955982984276
constante.c constante.c
1 #include <stdio.h>
2
3 int main(void)
4 {
5 const float pi = 3.14;
6 float r, a;
7
11 a = pi * r * r;
12
15 return 0;
16 }
Pero la posibilidad de declarar constantes con const no nos libra de la directiva define,
pues no son de aplicación en todo lugar donde conviene usar una constante. Más adelante, al
estudiar la declaración de vectores, nos referiremos nuevamente a esta cuestión.
titulado ((El preprocesador y las constantes)) para saber qué son exactamente.
16 ¿Has leı́do ya el cuadro ((El preprocesador y las constantes))?
17 ¿A qué esperas para leer el cuadro ((El preprocesador y las constantes))?
preprocesar.c preprocesar.c
1 #define PI 3.14
2
3 int main(void)
4 {
5 int a = PI ;
6 return 0;
7 }
el preprocesador lo transforma en este otro programa (sin modificar nuestro fichero). Puedes
comprobarlo invocando directamente al preprocesador:
$ cpp -P preprocesar.c
El resultado es esto:
1 int main(void)
2 {
3 int a = 3.14 ;
4 return 0;
5 }
Como puedes ver, una vez ((preprocesado)), no queda ninguna directiva en el programa y
la aparición del identificador PI ha sido sustituida por el texto 3.14. Un error tı́pico es
confundir un #define con una declaración normal de variables y, en consecuencia, poner
una asignación entre el identificador y el valor:
1 #define PI = 3.14
2
3 int main(void)
4 {
5 int a = PI ;
6 return 0;
7 }
1 int main(void)
2 {
3 int a = = 3.14 ;
4 return 0;
5 }
1) Cargar registros
2) Guardar registros
3) A~
nadir registro
4) Borrar registro
5) Modificar registro
6) Buscar registro
7) Finalizar
Cuando el usuario escoge una opción, la almacenamos en una variable (llamémosla opcion) y
seleccionamos las sentencias a ejecutar con una serie de comparaciones como las que se muestran
aquı́ esquemáticamente18 :
if (opcion == 1) {
Código para cargar registros
}
else if (opcion == 2) {
Código para guardar registros
}
else if (opcion == 3) {
...
El código resulta un tanto ilegible porque no vemos la relación entre los valores numéricos y las
opciones de menú. Es frecuente no usar los literales numéricos y recurrir a constantes:
#define CARGAR 1
#define GUARDAR 2
#define ANYADIR 3
#define BORRAR 4
#define MODIFICAR 5
#define BUSCAR 6
#define FINALIZAR 7
...
if (opcion == CARGAR) {
Código para cargar registros
}
else if (opcion == GUARDAR) {
Código para guardar registros
}
else if (opcion == ANYADIR) {
...
Puedes ahorrarte la retahı́la de #defines con los denominados tipos enumerados. Un tipo
enumerado es un conjunto de valores ((con nombre)). Fı́jate en este ejemplo:
enum { Cargar =1, Guardar , Anyadir , Borrar , Modificar , Buscar , Finalizar };
...
if (opcion == Cargar ) {
Código para cargar registros
}
else if (opcion == Guardar ) {
Código para guardar registros
}
else if (opcion == Anyadir ) {
...
La primera lı́nea define los valores Cargar , Guardar , . . . como una sucesión de valores
correlativos. La asignación del valor 1 al primer elemento de la enumeración hace que la sucesión
empiece en 1. Si no la hubiésemos escrito, la sucesión empezarı́a en 0.
Es habitual que los enum aparezcan al principio del programa, tras la aparición de los
#include y #define.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 33 ¿Qué valor tiene cada identificador de este tipo enumerado?
enum { Primera=’a’, Segunda, Tercera, Penultima=’y’, Ultima };
(No te hemos explicado qué hace la segunda asignación. Comprueba que la explicación que das
es correcta con un programa que muestre por pantalla el valor de cada identificador.)
18 Más adelante estudiaremos una estructura de selección que no es if y que se usa normalmente para especificar
.............................................................................................
Los tipos enumerados sirven para algo más que asignar valores a opciones de menú. Es
posible definir identificadores con diferentes valores para series de elementos como los dı́as de
la semana, los meses del año, etc.
enum { Lunes, Martes, Miercoles, Jueves, Viernes, Sabado, Domingo };
enum { Invierno, Primavera, Verano, Otonyo };
enum { Rojo, Verde, Azul };
Con ella se indica que el programa hace uso de una biblioteca cuyas funciones, variables, tipos
de datos y constantes están declaradas en el fichero stdio.h, que es abreviatura de ((standard
input/output)) (entrada/salida estándar). En particular, el programa sumatorio.c usa las fun-
ciones printf y scanf de stdio.h. Los ficheros con extensión ((.h)) se denominan ficheros cabecera
(la letra h es abreviatura de ((header)), que en inglés significa ((cabecera))).
A diferencia de Python, C no permite importar un subconjunto de las funciones propor-
cionadas por una biblioteca. Al hacer #include de una cabecera se importan todas sus fun-
ciones, tipos de datos, variables y constantes. Es como si en Python ejecutaras la sentencia
from módulo import *.
Normalmente no basta con incluir un fichero de cabecera con #include para poder compilar
un programa que utiliza bibliotecas. Es necesario, además, compilar con opciones especiales.
Abundaremos sobre esta cuestión inmediatamente, al presentar la librerı́a matemática.
Constante Valor
M_E una aproximación del número e
M_PI una aproximación del número π
M_PI_2 una aproximación de π/2
M_PI_4 una aproximación de π/4
M_1_PI una aproximación de √
1/π
M_SQRT2 una aproximación de 2
M_LOG2E una aproximación de log2 e
M_LOG10E una aproximación de log10 e
No basta con escribir #include <math.h> para poder usar las funciones matemáticas: has
de compilar con la opción -lm:
$ gcc programa.c -lm -o programa
¿Por qué? Cuando haces #include, el preprocesador introduce un fragmento de texto que
dice qué funciones pasan a estar accesibles, pero ese texto no dice qué hace cada función y cómo
lo hace (con qué instrucciones concretas). Si compilas sin -lm, el compilador se ((quejará)):
$ gcc programa.c -o programa
/tmp/ccm1nE0j.o: In function ‘main’:
/tmp/ccm1nE0j.o(.text+0x19): undefined reference to ‘sqrt’
collect2: ld returned 1 exit status
El mensaje advierte de que hay una ((referencia indefinida a sqrt)). En realidad no se está
((quejando)) el compilador, sino otro programa del que aún no te hemos dicho nada: el enlazador
(en inglés, ((linker))). El enlazador es un programa que detecta en un programa las llamadas a
función no definidas en un programa C y localiza la definición de las funciones (ya compiladas) en
bibliotecas. El fichero math.h que incluı́mos con #define contiene la cabecera de las funciones
matemáticas, pero no su cuerpo. El cuerpo de dichas funciones, ya compilado (es decir, en
código de máquina), reside en otro fichero: /usr/lib/libm.a. ¿Para qué vale el fichero math.h
si no tiene el cuerpo de las funciones? Para que el compilador compruebe que estamos usando
correctamente las funciones (que suministramos el número de argumentos adecuado, que su
tipo es el que debe ser, etc.). Una vez que se comprueba que el programa es correcto, se procede
a generar el código de máquina, y ahı́ es necesario ((pegar)) (((enlazar))) el código de máquina de
las funciones matemáticas que hemos utilizado. El cuerpo ya compilado de sqrt, por ejemplo,
se encuentra en /usr/lib/libm.a (libm es abreviatura de ((math library))). El enlazador es el
programa que ((enlaza)) el código de máquina de nuestro programa con el código de máquina de
las bibliotecas que usamos. Con la opción -lm le indicamos al enlazador que debe resolver las
referencias indefinidas a funciones matemáticas utilizando /usr/lib/libm.a.
La opción -lm evita tener que escribir /usr/lib/libm.a al final. Estas dos invocaciones del
compilador son equivalentes:
$ gcc programa.c -o programa -lm
$ gcc programa.c -o programa /usr/lib/libm.a
Con -L le indicamos al compilador que debe enlazar llamadas a funciones cuyo código
no se proporciona en programa.c a funciones definidas y disponibles (ya traducidas a código
de máquina) en un fichero determinado (en el ejemplo, /usr/lib/libm.a). Como la librerı́a
matemática se usa tan frecuentemente y resulta pesado escribir la ruta completa a libm.a, gcc
nos permite usar la abreviatura -lm.
El proceso completo de compilación cuando enlazamos con /usr/lib/libm.a puede repre-
sentarse gráficamente ası́:
programa.c Preprocesador Compilador Enlazador programa
/usr/lib/libm.a
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 34 Diseña un programa C que solicite la longitud p de los tres lados de un triángulo (a, b y c)
y muestre por pantalla su perı́metro y su área ( s(s − a)(s − b)(s − c), donde s = (a+b+c)/2.).
Compila y ejecuta el programa.
· 35 Diseña un programa C que solicite el radio r de una circunferencia y muestre por
pantalla su perı́metro (2πr) y su área (πr2 ). Utiliza la aproximación a π predefinida en la
biblioteca matemática.
Compila y ejecuta el programa.
.............................................................................................
Los paréntesis que encierran a la condición son obligatorios. Como en Python no lo son, es fácil
que te equivoques por no ponerlos. Si el bloque de sentencias consta de una sola sentencia, no
es necesario encerrarla entre llaves:
if (condición)
sentencia;
Si uno de los bloques sólo tiene una sentencia, generalmente puedes eliminar las llaves:
if (condición)
sentencia_si;
else {
sentencias_no
}
if (condición) {
sentencias_si
}
else
sentencia_no;
if (condición)
sentencia_si;
else
sentencia_no;
¿A cuál de los dos if pertenece el else? ¿Hará el compilador de C una interpretación como la
que sugiere la indentación en el último fragmento o como la que sugiere este otro?:
if (condición)
if (otra_condición) {
sentencias_si
}
???
else { // ???
sentencias_no
}
C rompe la ambigüedad trabajando con esta sencilla regla: el else pertenece al if ((libre))
más cercano. Si quisiéramos expresar la primera estructura, deberı́amos añadir llaves para
determinar completamente qué bloque está dentro de qué otro:
if (condición) {
if (otra_condición) {
sentencias_si
}
}
else {
sentencias_no
}
El if externo contiene una sola sentencia (otro if ) y, por tanto, las llaves son redundantes;
pero hacen hacen evidente que el else va asociado a la condición exterior.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 36 Diseña un programa C que pida por teclado un número entero y diga si es par o impar.
· 37 Diseña un programa que lea dos números enteros y muestre por pantalla, de estos tres
mensajes, el que convenga:
1 switch (expresión) {
2 case valor 1:
3 sentencias
4 break;
5 case valor 2:
6 sentencias
7 break;
8 ...
9 default:
10 sentencias
11 break;
12 }
3 int main(void)
4 {
5 int opcion;
6
5 int main(void)
6 {
7 int opcion;
8
5 int main(void)
6 {
7 int opcion;
8
5 int main(void)
6 {
7 int opcion;
8
Nuevamente, los paréntesis son obligatorios y las llaves pueden suprimirse si el bloque contiene
una sola sentencia.
Veamos un ejemplo de uso: un programa que calcula xn para x y n enteros:
potencia.c potencia.c
1 #include <stdio.h>
2
3 int main(void)
4 {
5 int x, n, i, r;
6
17 return 0;
18 }
El bucle do-while
Hay un bucle iterativo que Python no tiene: el do-while:
do {
sentencias
} while (condición);
El bucle do-while evalúa la condición tras cada ejecución de su bloque, ası́ que es seguro
que éste se ejecuta al menos una vez. Podrı́amos reescribir sumatorio.c para usar un bucle
do-while:
sumatorio 2.c sumatorio.c
1 #include <stdio.h>
2 #include <math.h>
3
4 int main(void)
5 {
6 int a, b, i;
7 float s;
8
15 do {
16 printf ("Lı́mite superior:"); scanf ("%d", &b);
17 if (b < a) printf ("No puede ser menor que %d\n", a);
18 } while (b < a);
19
24 /* Mostrar el resultado. */
25 printf ("Sumatorio de raı́ces de %d a %d: %f\n", a, b, s);
26
27 return 0;
28 }
Los bucles do-while no añaden potencia al lenguaje, pero sı́ lo dotan de mayor expresividad.
Cualquier cosa que puedas hacer con bucles do-while, puedes hacerla también con sólo bucles
while y la ayuda de alguna sentencia condicional if , pero probablemente requerirán mayor
esfuerzo por tu parte.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 44 Escribe un programa que muestre un menú en pantalla con dos opciones: ((saludar)) y
((salir)). El programa pedirá al usuario una opción y, si es válida, ejecutará su acción asociada.
Mientras no se seleccione la opción ((salir)), el menú reaparecerá y se solicitará nuevamente una
opción. Implementa el programa haciendo uso únicamente de bucles do-while.
Todos los números se irán mostrando por pantalla conforme se vayan generando. El proceso se
repetirá hasta que el número generado sea igual a 1. Utiliza un bucle do-while.
.............................................................................................
Comparaciones y asignaciones
Un error frecuente es sustituir el operador de comparación de igualdad por el de asignación
en una estructura if o while. Analiza este par de sentencias:
a=0
?
if (a = 0) { // Lo que escribió... bien o mal?
...
}
Parece que la condición del if se evalúa a cierto, pero no es ası́: la ((comparación)) es, en
realidad, una asignación. El resultado es que a recibe el valor 0 y que ese 0, devuelto por el
operador de asignación, se considera la representación del valor ((falso)). Lo correcto hubiera
sido:
a=0
if (a == 0) { // Lo que querı́a escribir.
...
}
a=0
if (0 == a) { // Correcto.
...
}
De ese modo, si se confunden y usan = en lugar de ==, se habrá escrito una expresión
incorrecta y el compilador detendrá el proceso de traducción a código de máquina:
a=0
if (0 = a) { // Mal: error detectable por el compilador.
...
}
El bucle for
El bucle for de Python existe en C, pero con importantes diferencias.
Los paréntesis de la primera lı́nea son obligatorios. Fı́jate, además, en que los tres elementos
entre paréntesis se separan con puntos y comas.
El bucle for presenta tres componentes. Es equivalente a este fragmento de código:
inicialización;
while (condición) {
sentencias
incremento;
}
Una forma habitual de utilizar el bucle for es la que se muestra en este ejemplo, que imprime
por pantalla los números del 0 al 9 y en el que suponemos que i es de tipo int:
for (i = 0; i < 10; i++) {
printf ("%d\n", i);
}
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 46 Implementa el programa de cálculo de xn (para x y n entero) con un bucle for.
· 47 Implementa un programa que dado un número de tipo int, leı́do por teclado, se asegure
de que sólo contiene ceros y unos y muestre su valor en pantalla si lo interpretamos como un
número binario. Si el usuario introduce, por ejemplo, el número 1101, el programa mostrará el
valor 13. Caso de que el usuario instroduzca un número formado por números de valor diferente,
indı́ca al usuario que no puedes proporcionar el valor de su interpretación como número binario.
· 48 Haz un programa que solicite un número entero y muestre su factorial. Utiliza un entero
de tipo long long para el resultado. Debes usar un bucle for.
· 49 El número de combinaciones de n elementos tomados de m en m es:
m n n!
Cn = = .
m (n − m)! m!
Diseña un programa que pida el valor de n y m y calcule Cnm . (Ten en cuenta que n ha de ser
mayor o igual que m.)
(Puedes comprobar la validez de tu programa introduciendo los valores n = 15 y m = 10:
el resultado es 3003.)
· 50 ¿Qué muestra por pantalla este programa?
desplazamientos.c desplazamientos.c
1 #include <stdio.h>
2
3 int main(void)
4 {
5 int a = 127, b = 1024, c, i;
6
7 c = a ^ b;
8
10
11 a = 2147483647;
12 for (i = 0; i < 8*sizeof (a); i++) {
13 printf ("%d", ((c & a) != 0) ? 1 : 0);
14 a >>= 1;
15 }
16 printf ("\n");
17
18 a = 1;
19 for (i = 0; i < 8*sizeof (a); i++) {
20 if ((c & a) != 0) c >>= 1;
21 else c <<= 1;
22 a <<= 1;
23 }
24
25 a = 2147483647;
26 for (i = 0; i < 8*sizeof (a); i++) {
27 printf ("%d", ((c & a) != 0) ? 1 : 0);
28 a >>= 1;
29 }
30 printf ("\n");
31 return 0;
32 }
· 51 Cuando no era corriente el uso de terminales gráficos de alta resolución era común
representar gráficas de funciones con el terminal de caracteres. Por ejemplo, un periodo de la
función seno tiene este aspecto al representarse en un terminal de caracteres (cada punto es un
asterisco):
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
Haz un programa C que muestre la función seno utilizando un bucle que recorre el periodo 2π
en 24 pasos (es decir, representándolo con 24 lı́neas).
· 52 Modifica el programa para que muestre las funciones seno (con asteriscos) y coseno (con
sumas) simultáneamente.
.............................................................................................
Hacer un bucle que recorra, por ejemplo, los números pares entre 0 y 10 es sencillo: basta
sustituir el modo en que se incrementa la variable ı́ndice:
for (i = 0; i < 10; i = i + 2) {
printf ("%d\n", i);
}
aunque la forma habitual de expresar el incremento de i es esta otra:
for (i = 0; i < 10; i += 2) {
printf ("%d\n", i);
}
Un bucle que vaya de 10 a 1 en orden inverso presenta este aspecto:
for (i = 10; i > 0; i--) {
printf ("%d\n", i);
}
3 int main(void)
4 {
5 int a = 1;
6
12 return 0;
13 }
La variable i, el ı́ndice del bucle, se declara en la mismı́sima zona de inicialización del bucle.
La variable i sólo existe en el ámbito del bucle, que es donde se usa.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 53 Diseña un programa C que muestre el valor de 2n para todo n entre 0 y un valor entero
proporcionado por teclado.
· 54 Haz un programa que pida al usuario una cantidad de euros, una tasa de interés y
un número de años y muestre por pantalla en cuánto se habrá convertido el capital inicial
transcurridos esos años si cada año se aplica la tasa de interés introducida.
Recuerda que un capital C a un interés del x por cien durante n años se convierte en
C · (1 + x/100)n .
(Prueba tu programa sabiendo que 10 000 euros al 4.5% de interés anual se convierten en
24 117.14 euros al cabo de 20 años.)
· 55 Un vector en un espacio tridimensional es una tripleta de valores reales (x, y, z). Desea-
mos confeccionar un programa que permita operar con dos vectores. El usuario verá en pantalla
un menú con las siguientes opciones:
1) Introducir el primer vector
2) Introducir el segundo vector
3) Calcular la suma
4) Calcular la diferencia
5) Calcular el producto vectorial
6) Calcular el producto escalar
7) Calcular el ángulo (en grados) entre ellos
8) Calcular la longitud
9) Finalizar
Tras la ejecución de cada una de las acciones del menú éste reaparecerá en pantalla, a menos
que la opción escogida sea la número 9. Si el usuario escoge una opción diferente, el programa
advertirá al usuario de su error y el menú reaparecerá.
Las opciones 4 y 5 pueden proporcionar resultados distintos en función del orden de los
operandos, ası́ que, si se escoge cualquiera de ellas, aparecerá un nuevo menú que permita
seleccionar el orden de los operandos. Por ejemplo, la opción 4 mostrará el siguiente menú:
1) Primer vector menos segundo vector
2) Segundo vector menos primer vector
Puede que necesites que te refresquemos la memoria sobre los cálculos a realizar. Quizá la
siguiente tabla te sea de ayuda:
Operación Cálculo
Suma: (x1 , y1 , z1 ) + (x2 , y2 , z2 ) (x1 + x2 , y1 + y2 , z1 + z2 )
Diferencia: (x1 , y1 , z1 ) − (x2 , y2 , z2 ) (x1 − x2 , y1 − y2 , z1 − z2 )
Producto escalar: (x1 , y1 , z1 ) · (x2 , y2 , z2 ) x1 x2 + y1 y2 + z1 z2
Producto vectorial: (x1 , y1 , z1 ) × (x2 , y2 , z2 ) (y1 z2 − z1 y2 , z1 x2 − x1 z2 , x1 y2 − y1 x2 )
!
180 x1 x2 + y1 y2 + z1 z2
Ángulo entre (x1 , y1 , z1 ) y (x2 , y2 , z2 ) · arccos p p
π x1 + y12 + z12 x22 + y22 + z22
2
p
Longitud de (x, y, z) x2 + y 2 + z 2
Ten en cuenta que tu programa debe contemplar toda posible situación excepcional: divi-
siones por cero, raı́ces con argumento negativo, etc..
.............................................................................................
continue.c continue.c
1 #include <stdio.h>
2
3 int main(void)
4 {
5 int i;
6
7 i = 0;
8 while (i < 10) {
9 if (i % 2 == 0) {
10 i++;
11 continue;
12 }
13 printf ("%d\n", i);
14 i++;
15 }
16
—Me llamo Alicia, Majestad —dijo Alicia con mucha educación; pero añadió para sus
adentros: ((¡Vaya!, en realidad no son más que un mazo de cartas. ¡No tengo por qué
tenerles miedo!)).
En este capı́tulo vamos a estudiar algunas estructuras que agrupan varios datos, pero cuyo
tamaño resulta conocido al compilar el programa y no sufre modificación alguna durante su
ejecución. Empezaremos estudiando los vectores, estructuras que se pueden asimilar a las listas
Python. En C, las cadenas son un tipo particular de vector. Manejar cadenas en C resulta
más complejo y delicado que manejarlas en Python. Como contrapartida, es más fácil definir
en C vectores multidimensionales (como las matrices) que en Python. En este capı́tulo nos
ocuparemos también de ellos. Estudiaremos además los registros en C, que permiten definir
nuevos tipos como agrupaciones de datos de tipos no necesariamente idénticos. Los registros de
C son conceptualmente idénticos a los que estudiamos en Python.
El vector a comprende los elementos a[0], a[1], a[2], . . . , a[9], todos de tipo int. Al igual
que con las listas Python, los ı́ndices de los vectores C empiezan en cero.
En una misma lı́nea puedes declarar más de un vector, siempre que todos compartan el
mismo tipo de datos para sus componentes. Por ejemplo, en esta lı́nea se declaran dos vectores
de float, uno con 20 componentes y otro con 100:
Sin cortes
Los vectores C son mucho más limitados que las listas Python. A los problemas relacionados
con el tamaño fijo de los vectores o la homogeneidad en el tipo de sus elementos se une
una incomodidad derivada de la falta de operadores a los que nos hemos acostumbrado
como programadores Python. El operador de corte, por ejemplo, no existe en C. Cuando
en Python deseábamos extraer una copia de los elementos entre i y j de un vector a
escribı́amos a[i:j+1]. En C no hay operador de corte. . . ni operador de concatenación o
repetición, ni sentencias de borrado de elementos, ni se entienden como accesos desde el
final los ı́ndices negativos, ni hay operador de pertenencia, etc. Echaremos de menos muchas
de las facilidades propias de Python.
Se considera mal estilo declarar la talla de los vectores con literales de entero. Es preferible
utilizar algún identificador para la talla, pero teniendo en cuenta que éste debe corresponder a
una constante:
#define TALLA 80
...
char a[TALLA];
Esta otra declaración es incorrecta, pues usa una variable para definir la talla del vector1 :
int talla = 80;
...
!
char a[talla]; // No siempre es válido!
Puede que consideres válida esta otra declaración que prescinde de constantes definidas con
define y usa constantes declaradas con const, pero no es ası́:
const int talla = 80;
...
!
char a[talla]; // No siempre es válido!
Una variable const es una variable en toda regla, aunque de ((sólo lectura)).
3 #define TALLA 5
4
5 int main(void)
6 {
7 int i, a[TALLA];
8
sólo se conoce en tiempo de ejecución, pero sólo si el vector es una variable local a una función. Para evitar
confusiones, no haremos uso de esa caracterı́stica en este capı́tulo y lo consideraremos incorrecto.
Observa que el acceso a elementos del vector sigue la misma notación de Python: usamos
el identificador del vector seguido del ı́ndice encerrado entre corchetes. En una ejecución del
programa obtuvimos este resultado en pantalla (es probable que obtengas resultados diferentes
si repites el experimento):
1073909760
1075061012
1205
1074091790
1073941880
3 #define TALLA 10
4
5 int main(void)
6 {
7 int i, a[TALLA];
8
15 return 0;
16 }
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 67 Declara e inicializa un vector de 100 elementos de modo que los componentes de ı́ndice
par valgan 0 y los de ı́ndice impar valgan 1.
· 68 Escribe un programa C que almacene en un vector los 50 primeros números de Fibonacci.
Una vez calculados, el programa los mostrará por pantalla en orden inverso.
· 69 Escribe un programa C que almacene en un vector los 50 primeros números de Fibonacci.
Una vez calculados, el programa pedirá al usuario que introduzca un número y dirá si es o no
es uno de los 50 primeros números de Fibonacci.
.............................................................................................
Hay una forma alternativa de inicializar vectores. En este fragmento se definen e inicializan
dos vectores, uno con todos sus elementos a 0 y otro con una secuencia ascendente de números:
1 #define TALLA 5
2 ...
3 int a[TALLA] = {0, 0, 0, 0, 0};
4 int b[TALLA] = {1, 2, 3, 4, 5};
Ten en cuenta que, al declarar e inicializar simultáneamente un vector, debes indicar explı́ci-
tamente los valores del vector y, por tanto, esta aproximación sólo es factible para la inicializa-
ción de unos pocos valores.
1 #include <stdio.h>
2
3 int main(void)
4 {
5 int i, a[ 10 ], b[ 10 ];
6
Las tallas de los vectores a y b aparecen en seis lugares diferentes: en sus declaraciones,
en los bucles que los inicializan y en los que se imprimen. Imagina que deseas modificar
el programa para que a pase a tener 20 enteros: tendrás que modificar sólo tres de esos
dieces. Ello te obliga a leer el programa detenidamente y, cada vez que encuentres un diez,
pararte a pensar si ese diez en particular corresponde o no a la talla de a. Innecesariamente
complicado. Estudia esta alternativa:
1 #include <stdio.h>
2
3 #define TALLA_A 10
4 #define TALLA_B 10
5
6 int main(void)
7 {
8 int i, a[ TALLA_A ], b[ TALLA_B ];
9
Si ahora necesitas modificar a para que tenga 20 elementos, basta con que edites la lı́nea
3 sustituyendo el 10 por un 20. Mucho más rápido y con mayor garantı́a de no cometer
errores.
¿Por qué en Python no nos preocupó esta cuestión? Recuerda que en Python no habı́a
declaración de variables, que las listas podı́an modificar su longitud durante la ejecución de
los programas y que podı́as consultar la longitud de cualquier secuencia de valores con la
función predefinida len. Python ofrece mayores facilidades al programador, pero a un doble
precio: la menor velocidad de ejecución y el mayor consumo de memoria.
El compilador deduce que la talla del vector es 5, es decir, el número de valores que aparecen
a la derecha del igual. Te recomendamos que, ahora que estás aprendiendo, no uses esta
forma de declarar vectores: siempre que puedas, opta por una que haga explı́cito el tamaño
del vector.
En C99 es posible inicializar sólo algunos valores del vector. La sintaxis es un poco
enrevesada. Aquı́ tienes un ejemplo en el que sólo inicializamos el primer y último elementos
de un vector de talla 10:
consideramos que i es primo, y si no, que no lo es. Inicialmente, todas las celdas excepto la de
ı́ndice 0 valen 1. Entonces ((tachamos)) (ponemos un 0 en) las celdas cuyo ı́ndice es múltiplo
de 2. Acto seguido se busca la siguiente casilla que contiene un 1 y se procede a tachar todas
las casillas cuyo ı́ndice es múltiplo del ı́ndice de esta casilla. Y ası́ sucesivamente. Cuando se ha
recorrido completamente el vector, las casillas cuyo ı́ndice es primo contienen un 1.
Vamos con una primera versión del programa:
eratostenes.c eratostenes.c
1 #include <stdio.h>
2
3 #define N 100
4
5 int main(void)
6 {
7 int criba[ N ], i, j;
8
9 /* Inicialización */
10 criba[0] = 0;
11 for (i=1; i< N ; i++)
12 criba[i] = 1;
13
14 /* Criba de Eratóstenes */
15 for (i=2; i< N ; i++)
16 if (criba[i])
17 for (j=2; i*j< N ; j++)
18 criba[i*j] = 0;
19
25 return 0;
26 }
Observa que hemos tenido que decidir qué valor toma N, pues el vector criba debe tener un
tamaño conocido en el momento en el que se compila el programa. Si deseamos conocer los,
digamos, primos menores que 200, tenemos que modificar la lı́nea 3.
Mejoremos el programa. ¿Es necesario utilizar 4 bytes para almacenar un 0 o un 1? Estamos
malgastando memoria. Esta otra versión reduce a una cuarta parte el tamaño del vector criba:
3 #define N 100
4
5 int main(void)
6 {
7 char criba[N] ;
8 int i, j;
9
10 /* Inicialización */
11 criba[0] = 0;
12 for (i=1; i<N; i++)
13 criba[i] = 1;
14
15 /* Criba de Eratóstenes */
16 for (i=2; i<N; i++)
17 if (criba[i])
18 for (j=2; i*j<N; j++)
19 criba[i*j] = 0;
20
26 return 0;
27 }
Mejor ası́.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 70 Puedes ahorrar tiempo de ejecución haciendo que i tome valores entre 2 y la raı́z cuadrada
de N. Modifica el programa y comprueba que obtienes el mismo resultado.
.............................................................................................
3 #define PERSONAS 15
4
5 int main(void)
6 {
7 int edad [PERSONAS], i;
8
2 Hay una definición alternativa de la desviación tı́pica en la que el denominador de la fracción es 14.
4 #define N 100
5
6 int main(void)
7 {
8 char criba[N/8+1]; // Ocupa unas 8 veces menos que la versión anterior.
9 int i, j;
10
11 /* Inicialización */
12 criba[0] = 254; // Pone todos los bits a 1 excepto el primero.
13 for (i=1; i<=N/8; i++)
14 criba[i] = 255; // Pone todos los bits a 1.
15
16 /* Criba de Eratóstenes */
17 for (i=2; i<N; i++)
18 if (criba[i/8] & (1 << (i%8))) // Pregunta si el bit en posición i vale 1.
19 for (j=2; i*j<N; j++)
20 criba[i*j/8] &= ~(1 << ((i*j) % 8)); // Pone a 0 el bit en posición i*j.
21
27 return 0;
28 }
¡Buf! La legibilidad deja mucho que desear. Y no sólo eso: consultar si un determinado bit
vale 1 y fijar un determinado bit a 0 resultan ser operaciones más costosas que consultar
si el valor de un char es 1 o, respectivamente, fijar el valor de un char a 0, pues debes
hacerlo mediante operaciones de división entera, resto de división entera, desplazamiento,
negación de bits y el operador &.
¿Vale la pena reducir la memoria a una octava parte si, a cambio, el programa pierde
legibilidad y, además, resulta más lento? No hay una respuesta definitiva a esta pregunta. La
única respuesta es: depende. En según qué aplicaciones, puede resultar necesario, en otras
no. Lo que no debes hacer, al menos de momento, es obsesionarte con la optimización y
complicar innecesariamente tus programas.
9 /* Lectura de edades */
10 for (i=0; i<PERSONAS; i++) {
11 printf ("Por favor, introduce edad de la persona número %d: ", i+1);
12 scanf ("%d", &edad [i]);
13 }
14
15 return 0;
16 }
Vale la pena que te detengas a observar cómo indicamos a scanf que lea la celda de ı́ndice i en
el vector edad : usamos el operador & delante de la expresión edad [i]. Es lo que cabı́a esperar:
edad [i] es un escalar de tipo int, y ya sabes que scanf espera su dirección de memoria.
Pasamos ahora a calcular la edad media y la desviación tı́pica (no te ha de suponer dificultad
alguna con la experiencia adquirida al aprender Python):
4 #define PERSONAS 15
5
6 int main(void)
7 {
8 int edad [PERSONAS], i, suma_edad ;
9 float suma_desviacion, media, desviacion ;
10
11 /* Lectura de edades */
12 for (i=0; i<PERSONAS; i++) {
13 printf ("Por favor, introduce edad de la persona número %d: ", i+1);
14 scanf ("%d", &edad [i]);
15 }
16
17 /* Cálculo de la media */
18 suma_edad = 0;
19 for (i=0; i<PERSONAS; i++)
20 suma_edad += edad [i];
21 media = suma_edad / (float) PERSONAS;
22
29 /* Impresión de resultados */
30 printf ("Edad media : %f\n", media);
31 printf ("Desv. tı́pica: %f\n", desviacion);
32
33 return 0;
34 }
El cálculo de la moda (la edad más frecuente) resulta más problemática. ¿Cómo abordar el
cálculo? Vamos a presentar dos versiones diferentes. Empezamos por una que consume dema-
siada memoria. Dado que trabajamos con edades, podemos asumir que ninguna edad iguala o
supera los 150 años. Podemos crear un vector con 150 contadores, uno para cada posible edad:
edades 2.c edades.c
1 #include <stdio.h>
2 #include <math.h>
3
4 #define PERSONAS 15
5 #define MAX_EDAD 150
6
7 int main(void)
8 {
9 int edad [PERSONAS], i, suma_edad ;
10 float suma_desviacion, media, desviacion;
11 int contador [MAX_EDAD], frecuencia, moda;
12
13 /* Lectura de edades */
14 for (i=0; i<PERSONAS; i++) {
15 printf ("Por favor, introduce edad de la persona número %d: ", i+1);
16 scanf ("%d", &edad [i]);
17 }
18
19 /* Cálculo de la media */
20 suma_edad = 0;
21 for (i=0; i<PERSONAS; i++)
22 suma_edad += edad [i];
31 /* Cálculo de la moda */
32 for (i=0; i<MAX_EDAD; i++) // Inicialización de los contadores.
33 contador [i] = 0;
34 for (i=0; i<PERSONAS; i++)
35 contador [edad [i]]++; // Incrementamos el contador asociado a edad [i].
36 moda = -1;
37 frecuencia = 0;
38 for (i=0; i<MAX_EDAD; i++) // Búsqueda de la moda (edad con mayor valor del contador).
39 if (contador [i] > frecuencia) {
40 frecuencia = contador [i];
41 moda = i;
42 }
43
44 /* Impresión de resultados */
45 printf ("Edad media : %f\n", media);
46 printf ("Desv. tı́pica: %f\n", desviacion);
47 printf ("Moda : %d\n", moda);
48
49 return 0;
50 }
Esta solución consume un vector de 150 elementos enteros cuando no es estrictamente ne-
cesario. Otra posibilidad pasa por ordenar el vector de edades y contar la longitud de cada
secuencia de edades iguales. La edad cuya secuencia sea más larga es la moda:
edades 3.c edades.c
1 #include <stdio.h>
2 #include <math.h>
3
4 #define PERSONAS 15
5
6 int main(void)
7 {
8 int edad [PERSONAS], i, j, aux , suma_edad ;
9 float suma_desviacion, media, desviacion;
10 int moda, frecuencia, frecuencia_moda ;
11
12 /* Lectura de edades */
13 for (i=0; i<PERSONAS; i++) {
14 printf ("Por favor, introduce edad de la persona número %d: ", i+1);
15 scanf ("%d", &edad [i]);
16 }
17
18 /* Cálculo de la media */
19 suma_edad = 0;
20 for (i=0; i<PERSONAS; i++)
21 suma_edad += edad [i];
22 media = suma_edad / (float) PERSONAS;
23
30 /* Cálculo de la moda */
39 frecuencia = 0;
40 frecuencia_moda = 0;
41 moda = -1;
42 for (i=0; i<PERSONAS-1; i++) // Búsqueda de la serie de valores idénticos más larga.
43 if (edad [i] == edad [i+1]) {
44 frecuencia++;
45 if (frecuencia > frecuencia_moda) {
46 frecuencia_moda = frecuencia;
47 moda = edad [i];
48 }
49 }
50 else
51 frecuencia = 0;
52
53 /* Impresión de resultados */
54 printf ("Edad media : %f\n", media);
55 printf ("Desv. tı́pica: %f\n", desviacion);
56 printf ("Moda : %d\n", moda);
57
58 return 0;
59 }
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 71 ¿Contiene en cada instante la variable frecuencia el verdadero valor de la frecuencia de
aparición de un valor? Si no es ası́, ¿qué contiene? ¿Afecta eso al cálculo efectuado? ¿Por qué?
· 72 Esta nueva versión del programa presenta la ventaja adicional de no fijar un lı́mite
máximo a la edad de las personas. El programa resulta, ası́, de aplicación más general. ¿Son
todo ventajas? ¿Ves algún aspecto negativo? Reflexiona sobre la velocidad de ejecución del
programa comparada con la del programa que consume más memoria.
.............................................................................................
Sólo nos resta calcular la mediana. Mmmm. No hay que hacer nuevos cálculos para conocer
la mediana: gracias a que hemos ordenado el vector, la mediana es el valor que ocupa la posición
central del vector, es decir, la edad de ı́ndice PERSONAS/2.
edades 4.c edades.c
1 #include <stdio.h>
2 #include <math.h>
3
4 #define PERSONAS 15
5
6 int main(void)
7 {
8 int edad [PERSONAS], i, j, aux , suma_edad ;
9 float suma_desviacion, media, desviacion;
10 int moda, frecuencia, frecuencia_moda, mediana ;
11
12 /* Lectura de edades */
13 for (i=0; i<PERSONAS; i++) {
14 printf ("Por favor, introduce edad de la persona número %d: ", i+1);
15 scanf ("%d", &edad [i]);
16 }
17
18 /* Cálculo de la media */
19 suma_edad = 0;
30 /* Cálculo de la moda */
31 for (i=0; i<PERSONAS-1; i++) // Ordenación mediante burbuja.
32 for (j=0; j<PERSONAS-i; j++)
33 if (edad [j] > edad [j+1]) {
34 aux = edad [j];
35 edad [j] = edad [j+1];
36 edad [j+1] = aux ;
37 }
38
39 frecuencia = 0;
40 frecuencia_moda = 0;
41 moda = -1;
42 for (i=0; i<PERSONAS-1; i++)
43 if (edad [i] == edad [i+1])
44 if ( ++ frecuencia > frecuencia_moda) { // Ver ejercicio 73.
45 frecuencia_moda = frecuencia;
46 moda = edad [i];
47 }
48 else
49 frecuencia = 0;
50
51 /* Cálculo de la mediana */
52 mediana = edad [PERSONAS/2]
53
54 /* Impresión de resultados */
55 printf ("Edad media : %f\n", media);
56 printf ("Desv. tı́pica: %f\n", desviacion);
57 printf ("Moda : %d\n", moda);
58 printf ("Mediana : %d\n", mediana);
59
60 return 0;
61 }
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 73 Fı́jate en la lı́nea 44 del programa y compárala con las lı́neas 44 y 45 de su versión
anterior. ¿Es correcto ese cambio? ¿Lo serı́a este otro?:
44 if (frecuencia++ > frecuencia_moda) {
.............................................................................................
Bueno, vamos a modificar ahora el programa para que el usuario introduzca cuantas edades
desee hasta un máximo de 20. Cuando se introduzca un valor negativo para la edad, entende-
remos que ha finalizado la introducción de datos.
edades.c
1 #include <stdio.h>
2 #include <math.h>
3
4 #define PERSONAS 20
5
6 int main(void)
7 {
8 int edad [PERSONAS], i, j, aux , suma_edad ;
9 float suma_desviacion, media, desviacion;
12 /* Lectura de edades */
13 for (i=0; i<PERSONAS; i++) {
14 printf ("Introduce edad de la persona %d (si es negativa, acaba): ", i+1);
15 scanf ("%d", &edad [i]);
16 if (edad [i] < 0)
17 break;
18 }
19
20 ...
21
22 return 0;
23 }
Mmmm. Hay un problema: si no damos 20 edades, el vector presentará toda una serie de valores
sin inicializar y, por tanto, con valores arbitrarios. Serı́a un grave error tomar esos valores por
edades introducidas por el usuario. Una buena idea consiste en utilizar una variable entera que
nos diga en todo momento cuántos valores introdujo realmente el usuario en el vector edad :
4 #define MAX_PERSONAS 20
5
6 int main(void)
7 {
8 int edad [MAX_PERSONAS], personas , i, j, aux , suma_edad ;
9 float suma_desviacion, media, desviacion;
10 int moda, frecuencia, frecuencia_moda, mediana;
11
12 /* Lectura de edades */
13 personas = 0 ;
14 for (i=0; i<MAX_PERSONAS; i++) {
15 printf ("Introduce edad de la persona %d (si es negativa, acabar): ", i+1);
16 scanf ("%d", &edad [i]);
17 if (edad [i] < 0)
18 break;
19 personas++ ;
20 }
21
22 ...
23
24 return 0;
25 }
4 #define MAX_PERSONAS 20
5
6 int main(void)
7 {
8 int edad [MAX_PERSONAS], personas , i, j, aux , suma_edad ;
9 float suma_desviacion, media, desviacion;
10 int moda, frecuencia, frecuencia_moda, mediana;
11
12 /* Lectura de edades */
13 personas = 0;
14 do {
15 printf ("Introduce edad de la persona %d (si es negativa, acabar): ", personas+1);
16 scanf ("%d", &edad [personas]);
17 personas++;
18 } while (personas < MAX_PERSONAS && edad [personas-1] >= 0);
19 personas--;
20
21 ...
22
23 return 0;
24 }
Imagina que se han introducido edades de 10 personas. La variable personas apunta (con-
ceptualmente) al final de la serie de valores que hemos de considerar para efectuar los cálculos
pertinentes:
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
personas 10
MAX PERSONAS 20
Ya podemos calcular la edad media, pero con un cuidado especial por las posibles divisiones
por cero que provocarı́a que el usuario escribiera una edad negativa como edad de la primera
persona (en cuyo caso personas valdrı́a 0):
edades 7.c edades.c
1 #include <stdio.h>
2 #include <math.h>
3
4 #define MAX_PERSONAS 20
5
6 int main(void)
7 {
8 int edad [MAX_PERSONAS], personas, i, j, aux , suma_edad ;
9 float suma_desviacion, media, desviacion;
10 int moda, frecuencia, frecuencia_moda, mediana;
11
12 /* Lectura de edades */
13 personas = 0;
14 do {
15 printf ("Introduce edad de la persona %d (si es negativa, acabar): ", personas+1);
16 scanf ("%d", &edad [personas]);
17 personas++;
18 } while (personas < MAX_PERSONAS && edad [personas-1] >= 0);
19 personas--;
20
21 if (personas > 0) {
22 /* Cálculo de la media */
23 suma_edad = 0;
24 for (i=0; i< personas ; i++)
25 suma_edad += edad [i];
26 media = suma_edad / (float) personas ;
27
34 /* Cálculo de la moda */
35 for (i=0; i< personas -1; i++) // Ordenación mediante burbuja.
36 for (j=0; j< personas -i; j++)
37 if (edad [j] > edad [j+1]) {
38 aux = edad [j];
39 edad [j] = edad [j+1];
40 edad [j+1] = aux ;
41 }
42
43 frecuencia = 0;
44 frecuencia_moda = 0;
45 moda = -1;
46 for (i=0; i< personas -1; i++)
47 if (edad [i] == edad [i+1])
48 if (++frecuencia > frecuencia_moda) {
49 frecuencia_moda = frecuencia;
50 moda = edad [i];
51 }
52 else
53 frecuencia = 0;
54
55 /* Cálculo de la mediana */
56 mediana = edad [ personas /2];
57
58 /* Impresión de resultados */
59 printf ("Edad media : %f\n", media);
60 printf ("Desv. tı́pica: %f\n", desviacion);
61 printf ("Moda : %d\n", moda);
62 printf ("Mediana : %d\n", mediana);
63 }
64 else
65 printf ("No se introdujo dato alguno.\n");
66
67 return 0;
68 }
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 74 Cuando el número de edades es par no hay elemento central en el vector ordenado, ası́
que estamos escogiendo la mediana como uno cualquiera de los elementos ((centrales)). Utiliza
una definición alternativa de edad mediana que considera que su valor es la media de las dos
edades que ocupan las posiciones más próximas al centro.
· 75 Modifica el ejercicio anterior para que, caso de haber dos o más valores con la máxima
frecuencia de aparición, se muestren todos por pantalla al solicitar la moda.
· 76 Modifica el programa anterior para que permita efectuar cálculos con hasta 100 personas.
· 77 Modifica el programa del ejercicio anterior para que muestre, además, cuántas edades
hay entre 0 y 9 años, entre 10 y 19, entre 20 y 29, etc. Considera que ninguna edad es igual o
superior a 150.
Ejemplo: si el usuario introduce las siguientes edades correspondientes a 12 personas:
10 23 15 18 20 18 57 12 29 31 78 28
el programa mostrará (además de la media, desviación tı́pica, moda y mediana), la siguiente
tabla:
0 - 9: 0
10 - 19: 5
20 - 29: 4
30 - 39: 1
40 - 49: 0
50 - 59: 1
60 - 69: 0
70 - 79: 1
80 - 89: 0
90 - 99: 0
100 - 109: 0
110 - 119: 0
120 - 129: 0
130 - 139: 0
140 - 149: 0
0 - 9:
10 - 19: *****
20 - 29: ****
30 - 39: *
40 - 49:
50 - 59: *
60 - 69:
70 - 79: *
80 - 89:
90 - 99:
100 - 109:
110 - 119:
120 - 129:
130 - 139:
140 - 149:
10 - 19: *****
20 - 29: ****
30 - 39: *
40 - 49:
50 - 59: *
60 - 69:
70 - 79: *
· 80 Modifica el programa del ejercicio anterior para que muestre el mismo histograma de
esta otra forma:
| ######### | | | | | | |
| ######### | ######### | | | | | |
| ######### | ######### | | | | | |
| ######### | ######### | | | | | |
| ######### | ######### | ######### | | ######### | | ######### |
+-----------+-----------+-----------+-----------+-----------+-----------+-----------+
| 10 - 19 | 20 - 29 | 30 - 39 | 40 - 49 | 50 - 59 | 60 - 69 | 70 - 79 |
.............................................................................................
polinomios.c
1 #include <stdio.h>
2 #define TALLA_POLINOMIO 11
3
4 int main(void)
5 {
6 float p[TALLA_POLINOMIO], q[TALLA_POLINOMIO];
7 ...
Como leer por teclado 11 valores para p y 11 más para q es innecesario cuando trabajamos
con polinomios de grado menor que 10, nuestro programa leerá los datos pidiendo en primer
lugar el grado de cada uno de los polinomios y solicitando únicamente el valor de los coeficientes
de grado menor o igual que el indicado:
E polinomios.c E
1 #include <stdio.h>
2
3 #define TALLA_POLINOMIO 11
4
5 int main(void)
6 {
7 float p[TALLA_POLINOMIO], q[TALLA_POLINOMIO];
8 int grado;
9 int i;
10
11 /* Lectura de p */
12 do {
13 printf ("Grado de p (entre 0 y %d): ", TALLA_POLINOMIO-1); scanf ("%d", &grado);
14 } while (grado < 0 || grado >= TALLA_POLINOMIO);
15 for (i = 0; i<=grado; i++) {
16 printf ("p %d: ", i); scanf ("%f", &p[i]);
17 }
18
19 /* Lectura de q */
20 do {
21 printf ("Grado de q (entre 0 y %d): ", TALLA_POLINOMIO-1); scanf ("%d", &grado);
22 } while (grado < 0 || grado >= TALLA_POLINOMIO);
23 for (i = 0; i<=grado; i++) {
24 printf ("q %d: ", i); scanf ("%f", &q[i]);
25 }
26
27 return 0;
28 }
11 /* Lectura de p */
12 do {
13 printf ("Grado de p (entre 0 y %d): ", TALLA_POLINOMIO-1); scanf ("%d", &grado);
14 } while (grado < 0 || grado >= TALLA_POLINOMIO);
15 for (i = 0; i<=grado; i++) {
16 printf ("p %d: ", i); scanf ("%f", &p[i]);
17 }
Ahora que hemos leı́do los polinomios, calculemos la suma. La almacenaremos en un nuevo
vector llamado s. La suma de dos polinomios de grado menor que TALLA_POLINOMIO es un
polinomio de grado también menor que TALLA_POLINOMIO, ası́ que el vector s tendrá talla
TALLA_POLINOMIO.
polinomios.c
4 ...
5 int main(void)
6 {
7 float p[TALLA_POLINOMIO], q[TALLA_POLINOMIO], s[TALLA_POLINOMIO] ;
8 ...
polinomios.c polinomios.c
1 #include <stdio.h>
2
3 #define TALLA_POLINOMIO 11
4
5 int main(void)
6 {
7 float p[TALLA_POLINOMIO], q[TALLA_POLINOMIO], s[TALLA_POLINOMIO];
8 int grado;
9 int i;
10
11 /* Lectura de p */
12 do {
13 printf ("Grado de p (entre 0 y %d): ", TALLA_POLINOMIO-1); scanf ("%d", &grado);
14 } while (grado < 0 || grado >= TALLA_POLINOMIO);
15 for (i = 0; i<=grado; i++) {
16 printf ("p %d: ", i); scanf ("%f", &p[i]);
17 }
18 for (i=grado+1; i<TALLA_POLINOMIO; i++)
19 p[i] = 0.0;
20
21 /* Lectura de q */
22 do {
23 printf ("Grado de q (entre 0 y %d): ", TALLA_POLINOMIO-1); scanf ("%d", &grado);
24 } while (grado < 0 || grado >= TALLA_POLINOMIO);
25 for (i = 0; i<=grado; i++) {
26 printf ("q %d: ", i); scanf ("%f", &q[i]);
27 }
28 for (i=grado+1; i<TALLA_POLINOMIO; i++)
29 q[i] = 0.0;
30
31 /* Cálculo de la suma */
32 for (i=0; i<TALLA_POLINOMIO; i++)
33 s[i] = p[i] + q[i];
34
41 return 0;
42 }
Aquı́ tienes un ejemplo de uso del programa con los polinomios p(x) = 5 + 3x + 5x2 + x3 y
q(x) = 4 − 4x − 5x2 + x3 :
Grado de p (entre 0 y 10): 3
p_0: 5
p_1: 3
p_2: 5
p_3: 1
Grado de q (entre 0 y 10): 3
q_0: 4
q_1: -4
q_2: -5
q_3: 1
Suma: 9.000000 + -1.000000 x^1 + 0.000000 x^2 + 2.000000 x^3 + 0.000000 x^4 +
0.000000 x^5 + 0.000000 x^6 + 0.000000 x^7 + 0.000000 x^8 + 0.000000 x^9 +
0.000000 x^10
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 81 Modifica el programa anterior para que no se muestren los coeficientes nulos.
· 82 Tras efectuar los cambios propuestos en el ejercicio anterior no aparecerá nada por
pantalla cuando todos los valores del polinomio sean nulos. Modifica el programa para que, en
tal caso, se muestre por pantalla 0.000000.
· 83 Tras efectuar los cambios propuestos en los ejercicios anteriores, el polinomio empieza
con un molesto signo positivo cuando s0 es nulo. Corrige el programa para que el primer término
del polinomio no sea precedido por el carácter +.
· 84 Cuando un coeficiente es negativo, por ejemplo −1, el programa anterior muestra su
correspondiente término en pantalla ası́: + -1.000 x^1. Modifica el programa anterior para
que un término con coeficiente negativo como el del ejemplo se muestre ası́: - 1.000000 x^1.
.............................................................................................
Nos queda lo más difı́cil: el producto de los dos polinomios. Lo almacenaremos en un vector
llamado m. Como el producto de dos polinomios de grado menor o igual que n es un polinomio
de grado menor o igual que 2n, la talla del vector m no es TALLA_POLINOMIO:
1 ...
2 int main(void)
3 {
4 float p[TALLA_POLINOMIO], q[TALLA_POLINOMIO], s[TALLA_POLINOMIO];
5 float m[2*TALLA_POLINOMIO-1] ;
6 ...
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 85 ¿Entiendes por qué hemos reservado 2*TALLA_POLINOMIO-1 elementos para m y no
2*TALLA_POLINOMIO?
.............................................................................................
El coeficiente mi , para valores de i entre 0 y el grado máximo de m(x), es decir, entre los
enteros 0 y 2*TALLA_POLINOMIO-2, se calcula ası́:
i
X
mi = pj · qi−j .
j=0
3 #define TALLA_POLINOMIO 11
4
5 int main(void)
6 {
7 float p[TALLA_POLINOMIO], q[TALLA_POLINOMIO], s[TALLA_POLINOMIO];
8 float m[2*TALLA_POLINOMIO-1];
9 int grado;
10 int i, j;
11
12 /* Lectura de p */
13 do {
14 printf ("Grado de p (entre 0 y %d): ", TALLA_POLINOMIO-1); scanf ("%d", &grado);
15 } while (grado < 0 || grado >= TALLA_POLINOMIO);
16 for (i = 0; i<=grado; i++) {
17 printf ("p %d: ", i); scanf ("%f", &p[i]);
18 }
19 for (i=grado+1; i<TALLA_POLINOMIO; i++)
20 p[i] = 0.0;
21
22 /* Lectura de q */
23 do {
24 printf ("Grado de q (entre 0 y %d): ", TALLA_POLINOMIO-1); scanf ("%d", &grado);
25 } while (grado < 0 || grado >= TALLA_POLINOMIO);
26 for (i = 0; i<=grado; i++) {
27 printf ("q %d: ", i); scanf ("%f", &q[i]);
28 }
29 for (i=grado+1; i<TALLA_POLINOMIO; i++)
30 q[i] = 0.0;
31
32 /* Cálculo de la suma */
33 for (i=0; i<TALLA_POLINOMIO; i++)
34 s[i] = p[i] + q[i];
35
56 return 0;
57 }
Observa que nos hubiera venido bien definir sendas funciones para la lectura y escritura de
los polinomios, pero al no saber definir funciones todavı́a, hemos tenido que copiar dos veces el
fragmento de programa correspondiente.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 86 El programa que hemos diseñado es ineficiente. Si, por ejemplo, trabajamos con polino-
mios de grado 5, sigue operando con los coeficientes correspondientes a x6 , x7 ,. . . , x10 , que son
nulos. Modifica el programa para que, con la ayuda de variables enteras, recuerde el grado de
los polinomios p(x) y q(x) en sendas variables talla_p y talla_q y use esta información en los
cálculos de modo que se opere únicamente con los coeficientes de los términos de grado menor
· 95 Diseña un programa C que almacene en un vector los 100 primeros números primos.
int a[5];
el compilador reserva una zona de memoria contigua capaz de albergar 5 valores de tipo int.
Como una variable de tipo int ocupa 4 bytes, el vector a ocupará 20 bytes.
Podemos comprobarlo con este programa:
1 #include <stdio.h>
2
3 #define TALLA 5
4
5 int main(void)
6 {
7 int a[TALLA];
8
Cada byte de la memoria tiene una dirección. Si, pongamos por caso, el vector a empieza
en la dirección 1000, a[0] se almacena en los bytes 1000–1003, a[1] en los bytes 1004–1007, y
ası́ sucesivamente. El último elemento, a[4], ocupará los bytes 1016–1019:
996:
1000: a[0]
1004: a[1]
1008: a[2]
1012: a[3]
1016: a[4]
1020:
Big-endian y little-endian
Lo bueno de los estándares es. . . que hay muchos donde elegir. No hay forma de ponerse de
acuerdo. Muchos ordenadores almacenan los números enteros de más de 8 bits disponiendo
los bits más significativos en la dirección de memoria más baja y otros, en la más alta. Los
primeros se dice que siguen la codificación ((big-endian)) y los segundos, ((little-endian)).
Pongamos un ejemplo. El número 67586 se representa en binario con cuatro bytes:
Supongamos que ese valor se almacena en los cuatro bytes que empiezan en la dirección
1000. En un ordenador ((big-endian)), se dispondrı́an en memoria ası́ (te indicamos bajo cada
byte su dirección de memoria):
Los ordenadores PC (que usan microprocesadores Intel y AMD), por ejemplo, son ((little-
endian)) y los Macintosh basados en microprocesadores Motorola son ((big-endian)). Aunque
nosotros trabajamos en clase con ordenadores Intel, te mostraremos los valores binarios
como estás acostumbrado a verlos: con el byte más significativo a la izquierda.
La diferente codificación de unas y otras plataformas plantea serios problemas a la hora
de intercambiar información en ficheros binarios, es decir, ficheros que contienen volcados
de la información en memoria. Nos detendremos nuevamente sobre esta cuestión cuando
estudiamos ficheros.
Por cierto, lo de ((little-endian)) y ((big-endian)) viene de ((Los viajes de Gulliver)), la novela
de Johnathan Swift. En ella, los liliputienses debaten sobre una importante cuestión polı́tica:
¿deben abrirse los huevos pasados por agua por su extremo grande, como defiende el partido
Big-Endian, o por su extremo puntiagudo, como mantiene el partido Little-Endian?
3 #define TALLA 5
4
5 int main(void)
6 {
7 int a[TALLA], i;
8
12 return 0;
13 }
3 #define TALLA 5
4
5 int main(void)
6 {
7 int a[TALLA], i;
8
12 return 0;
13 }
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 98 ¿Qué problema presenta esta otra versión del mismo programa?
lee vector 1.c lee vector.c
1 #include <stdio.h>
2
3 #define TALLA 5
4
5 int main(void)
6 {
7 int a[TALLA], i;
8
12 return 0;
13 }
.............................................................................................
Analiza este programa:
direcciones vector2.c direcciones vector2.c
1 #include <stdio.h>
2
3 #define TALLA 5
4
5 int main(void)
6 {
7 int a[TALLA], i;
8
Observa que la dirección de memoria de las lı́neas primera y última es la misma. En conse-
cuencia, esta lı́nea:
Ası́ pues, a expresa una dirección de memoria (la de su primer elemento), es decir, a es un puntero
o referencia a memoria y es equivalente a &a[0]. La caracterı́stica de que el identificador de un
vector represente, a la vez, al vector y a un puntero que apunta donde empieza el vector recibe
el nombre dualidad vector-puntero, y es un rasgo propio del lenguaje de programación C.
Representaremos esquemáticamente los vectores de modo similar a como representábamos
las listas en Python:
0 1 2 3 4
a 0 0 0 0 0
Fı́jate en que el gráfico pone claramente de manifiesto que a es un puntero, pues se le representa
con una flecha que apunta a la zona de memoria en la que se almacenan los elementos del vector.
Nos interesa diseñar programas con un nivel de abstracción tal que la imagen conceptual que
tengamos de los vectores se limite a la del diagrama.
a 0 0 0 0 0
Recuerda que el operador & obtiene la dirección de memoria en la que se encuentra un valor.
En esta figura te ilustramos &a[0] y &a[2] como sendos punteros a sus respectivas celdas en el
vector.
&a[2]
0 1 2 3 4
a 0 0 0 0 0
&a[0]
ilicito.c ilicito.c
1 #include <stdio.h>
2
3 #define TALLA 3
4
5 int main(void)
6 {
7 int v[TALLA], w[TALLA], i;
8
14 printf ("+--------+----------------------+-------+\n");
15 printf ("| Objeto | Dirección de memoria | Valor |\n");
16 printf ("+--------+----------------------+-------+\n");
17 printf ("| i | %20u | %5d |\n", (unsigned int) &i, i);
18 printf ("+--------+----------------------+-------+\n");
19 printf ("| w[0] | %20u | %5d |\n", (unsigned int) &w[0], w[0]);
20 printf ("| w[1] | %20u | %5d |\n", (unsigned int) &w[1], w[1]);
21 printf ("| w[2] | %20u | %5d |\n", (unsigned int) &w[2], w[2]);
22 printf ("+--------+----------------------+-------+\n");
23 printf ("| v[0] | %20u | %5d |\n", (unsigned int) &v[0], v[0]);
24 printf ("| v[1] | %20u | %5d |\n", (unsigned int) &v[1], v[1]);
25 printf ("| v[2] | %20u | %5d |\n", (unsigned int) &v[2], v[2]);
26 printf ("+--------+----------------------+-------+\n");
27 printf ("| v[-2] | %20u | %5d |\n", (unsigned int) &v[-2], v[-2]);
28 printf ("| v[-3] | %20u | %5d |\n", (unsigned int) &v[-3], v[-3]);
29 printf ("| v[-4] | %20u | %5d |\n", (unsigned int) &v[-4], v[-4]);
30 printf ("| w[5] | %20u | %5d |\n", (unsigned int) &w[5], w[5]);
31 printf ("| w[-1] | %20u | %5d |\n", (unsigned int) &w[-1], w[-1]);
32 printf ("| v[-5] | %20u | %5d |\n", (unsigned int) &v[-5], v[-5]);
33 printf ("+--------+----------------------+-------+\n");
34
35 return 0;
36 }
ordenado. La asignación de direcciones de memoria a cada objeto de un programa es una decisión que adopta
el compilador con cierta libertad.
| w[-1] | 3221222636 | 3 |
| v[-5] | 3221222636 | 3 |
+--------+----------------------+-------+
La salida es una tabla con tres columnas: en la primera se indica el objeto que se está
estudiando, la segunda corresponde a la dirección de memoria de dicho objeto4 y la tercera
muestra el valor almacenado en dicho objeto. A la vista de las direcciones de memoria de los
objetos i, v[0], v[1], v[2], w[0], w[1] y w[2], el compilador ha reservado la memoria de
estas variables ası́:
3221222636: 3 i
3221222640: 10 w[0]
3221222644: 11 w[1]
3221222648: 12 w[2]
3221222652:
3221222656: 0 v[0]
3221222660: 1 v[1]
3221222664: 2 v[2]
Fı́jate en que las seis últimas filas de la tabla corresponden a accesos a v y w con ı́ndices
fuera de rango. Cuando tratábamos de acceder a un elemento inexistente en una lista Python,
el intérprete generaba un error de tipo (error de ı́ndice). Ante una situación similar, C no
detecta error alguno. ¿Qué hace, pues? Aplica la fórmula de indexación, sin más. Estudiemos
con calma el primer caso extraño: v[-2]. C lo interpreta como: ((acceder al valor almacenado en
la dirección que resulta de sumar 3221222656 (que es donde empieza el vector v) a (−2) × 4 (−2
es el ı́ndice del vector y 4 es tamaño de un int))). Haz el cálculo: el resultado es 3221222648. . .
¡la misma dirección de memoria que ocupa el valor de w[2]! Esa es la razón de que se muestre
el valor 12. En la ejecución del programa, v[-2] y w[2] son exactamente lo mismo. Encuentra
tú mismo una explicación para los restantes accesos ilı́citos.
¡Ojo! Que se pueda hacer no significa que sea aconsejable hacerlo. En absoluto. Es más:
debes evitar acceder a elementos con ı́ndices de vector fuera de rango. Si no conviene hacer
algo ası́, ¿por qué no comprueba C si el ı́ndice está en el rango correcto antes de acceder a los
elementos y, en caso contrario, nos señala un error? Por eficiencia. Un programa que maneje
vectores accederá a sus elementos, muy probablemente, en numerosas ocasiones. Si se ha de
comprobar si el ı́ndice está en el rango de valores válidos, cada acceso se penalizará con un
par de comparaciones y el programa se ejecutará más lentamente. C sacrifica seguridad por
velocidad, de ahı́ que tenga cierta fama (justificadı́sma) de lenguaje ((peligroso)).
3 int main(void)
4 {
5 int original [TALLA] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10} ;
6 int copia[TALLA];
7
8 copia = original ;
9
10 return 0;
11 }
4 Si ejecutas el programa en tu ordenador, es probable que obtengas valores distintos para las direcciones de
memoria. Es normal: en cada ordenador y con cada ejecución se puede reservar una zona de memoria distinta
para los datos.
Violación de segmento
Los errores de acceso a zonas de memoria no reservada se cuentan entre los peores. En
el ejemplo, hemos accedido a la zona de memoria de un vector saliéndonos del rango de
indexación válido de otro, lo cual ha producido resultados desconcertantes.
Pero podrı́a habernos ido aún peor: si tratas de escribir en una zona de memoria que
no pertenece a ninguna de tus variables, cosa que puedes hacer asignando un valor a un
elemento de vector fuera de rango, es posible que se genere una excepción durante la
ejecución del programa: intentar escribir en una zona de memoria que no ha sido asignada
a nuestro proceso dispara, en Unix, una señal de ((violación de segmento)) (segmentation
violation) que provoca la inmediata finalización de la ejecución del programa. Fı́jate en este
programa:
violacion.c E violacion.c E
1 #include <stdio.h>
2
3 int main(void)
4 {
5 int a[10];
6
7 a[10000] = 1;
8
9 return 0;
10 }
Cuando lo ejecutamos en un ordenador bajo Unix, obtenemos este mensaje por pantalla:
Violación de segmento
0 1 2 3 4 5 6 7 8 9
original 1 2 3 4 5 6 7 8 9 10
0 1 2 3 4 5 6 7 8 9
copia
0 1 2 3 4 5 6 7 8 9
original 1 2 3 4 5 6 7 8 9 10
0 1 2 3 4 5 6 7 8 9
copia 1 2 3 4 5 6 7 8 9 10
2. o conseguir que, como en Python, copia apunte al mismo lugar que original :
0 1 2 3 4 5 6 7 8 9
original 1 2 3 4 5 6 7 8 9 10
0 1 2 3 4 5 6 7 8 9
copia
Pero no ocurre ninguna de las dos cosas: el identificador de un vector estático se considera un
puntero inmutable. Siempre apunta a la misma dirección de memoria. No puedes asignar un
vector a otro porque eso significarı́a cambiar el valor de su dirección. (Observa, además, que en
el segundo caso, la memoria asignada a copia quedarı́a sin puntero que la referenciara.)
Si quieres copiar el contenido de un vector en otro debes hacerlo elemento a elemento:
3 int main(void)
4 {
5 int original [TALLA] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10} ;
6 int copia[TALLA];
7 int i;
8
12 return 0;
13 }
3 int main(void)
4 {
5 int original [TALLA] = { 1, 2, 3 };
6 int copia[TALLA] = {1, 1+1, 3};
7 int i, son_iguales;
8
9 son_iguales = 1; // Suponemos que todos los elementos son iguales dos a dos.
10 i = 0;
11 while (i < TALLA && son_iguales) {
12 if (copia[i] != original [i]) // Pero basta con que dos elementos no sean iguales...
13 son_iguales = 0; // ... para que los vectores sean distintos.
14 i++;
15 }
16
17 if (son_iguales)
18 printf ("Son iguales\n");
19 else
20 printf ("No son iguales\n");
21
22 return 0;
23 }
a c a d e n a \0
Recuerda, pues, que hay dos valores relacionados con el tamaño de una cadena:
¿Y por qué toda esta complicación del terminador de cadena? Lo normal al trabajar con una
variable de tipo cadena es que su longitud varı́e conforme evoluciona la ejecución del programa,
pero el tamaño de un vector es fijo. Por ejemplo, si ahora tenemos en a el texto "cadena" y
más tarde decidimos guardar en ella el texto "texto", que tiene un carácter menos, estaremos
pasando de esta situación:
1 char a = ’y’;
2 char b[2] = "y";
a y
0 1
b y \0
Recuerda:
Las comillas simples definen un carácter y un carácter ocupa un solo byte.
Las comillas dobles definen una cadena. Toda cadena incluye un carácter nulo invisible
al final.
0 1 2 3 4 5 6 7 8 9
a c a d e n a \0
a esta otra:
0 1 2 3 4 5 6 7 8 9
a t e x t o \0
Fı́jate en que la zona de memoria asignada a a sigue siendo la misma. El ((truco)) del terminador
ha permitido que la cadena decrezca. Podemos conseguir también que crezca a voluntad. . . pero
siempre que no se rebase la capacidad del vector.
Hemos representado las celdas a la derecha del terminador como cajas vacı́as, pero no es
cierto que lo estén. Lo normal es que contengan valores arbitrarios, aunque eso no importa
mucho: el convenio de que la cadena termina en el primer carácter nulo hace que el resto de
caracteres no se tenga en cuenta. Es posible que, en el ejemplo anterior, la memoria presente
realmente este aspecto:
0 1 2 3 4 5 6 7 8 9
a t e x t o \0 a u \0 x
Por comodidad representaremos las celdas a la derecha del terminador con cajas vacı́as, pues
no importa en absoluto lo que contienen.
¿Qué ocurre si intentamos inicializar una zona de memoria reservada para sólo 10 chars con
una cadena de longitud mayor que 9?
!
char a[10] = "supercalifragilisticoespialidoso"; // Mal!
a s u p e r c a l i f r a g i l i s t i c o e s p i a l i d o s o \0
Ya vimos en un apartado anterior las posibles consecuencias de ocupar memoria que no nos ha
sido reservada: puede que modifiques el contenido de otras variables o que trates de escribir en
una zona que te está vetada, con el consiguiente aborto de la ejecución del programa.
Como resulta que en una variable con capacidad para, por ejemplo, 80 caracteres sólo
caben realmente 79 caracteres aparte del nulo, adoptaremos una curiosa práctica al declarar
variables de cadena que nos permitirá almacenar los 80 caracteres (además del nulo) sin crear
una constante confusión con respecto al número de caracteres que caben en ellas:
1 #include <stdio.h>
2
3 #define MAXLON 80
4
5 int main(void)
6 {
7 char cadena[ MAXLON+1 ]; /* Reservamos 81 caracteres: 80 caracteres más el terminador */
8
9 return 0;
10 }
3 #define MAXLON 80
4
5 int main(void)
6 {
7 char cadena[MAXLON+1] = "una cadena";
8
11 return 0;
12 }
3 #define MAXLON 80
4
5 int main(void)
6 {
7 char cadena[MAXLON+1] = "una cadena";
8
13 return 0;
14 }
¿Y si deseamos mostrar una cadena carácter a carácter? Podemos hacerlo llamando a printf
sobre cada uno de los caracteres, pero recuerda que la marca de formato asociada a un carácter
es %c:
salida caracter a caracter.c salida caracter a caracter.c
1 #include <stdio.h>
2
3 #define MAXLON 80
4
5 int main(void)
6 {
7 char cadena[MAXLON+1] = "una cadena";
8 int i;
9
10 i = 0;
11 while (cadena[i] != ’\0’) {
12 printf ("%c\n", cadena[i]);
13 i++;
14 }
15
16 return 0;
17 }
c
a
d
e
n
a
3 #define MAXLON 80
4
5 int main(void)
6 {
7 char cadena[MAXLON+1];
8
12 return 0;
13 }
¡Ojo! ¡No hemos puesto el operador & delante de cadena! ¿Es un error? No. Con las cadenas
no hay que poner el carácter & del identificador al usar scanf . ¿Por qué? Porque scanf espera
una dirección de memoria y el identificador, por la dualidad vector-puntero, ¡es una dirección
de memoria!
Recuerda: cadena[0] es un char, pero cadena, sin más, es la dirección de memoria en la
que empieza el vector de caracteres.
Ejecutemos el programa e introduzcamos una palabra:
una
La cadena leı́da es una
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 99 ¿Es válida esta otra forma de leer una cadena? Pruébala en tu ordenador.
1 #include <stdio.h>
2
3 #define MAXLON 80
4
5 int main(void)
6 {
7 char cadena[MAXLON+1];
8
12 return 0;
13 }
.............................................................................................
Cuando scanf recibe el valor asociado a cadena, recibe una dirección de memoria y, a partir
de ella, deja los caracteres leı́dos de teclado. Debes tener en cuenta que si los caracteres leı́dos
exceden la capacidad de la cadena, se producirá un error de ejecución.
¿Y por qué printf no muestra por pantalla una simple dirección de memoria cuando ejecuta-
mos la llamada printf ("La cadena leı́da es \%s.\n", cadena)? Si es cierto lo dicho, cadena
es una dirección de memoria. La explicación es que la marca %s es interpretada por printf como
((me pasan una dirección de memoria en la que empieza una cadena, ası́ que he de mostrar su
contenido carácter a carácter hasta encontrar un carácter nulo)).
3 #define MAXLON 80
4
5 int main(void)
6 {
7 char cadena[MAXLON+1];
8
12 return 0;
13 }
¿Qué ha ocurrido con los restantes caracteres tecleados? ¡Están a la espera de ser leı́dos!
La siguiente cadena leı́da, si hubiera un nuevo scanf , serı́a "frase". Si es lo que querı́amos,
perfecto, pero si no, el desastre puede ser mayúsculo.
¿Cómo leer, pues, una frase completa? No hay forma sencilla de hacerlo con scanf . Tendre-
mos que recurrir a una función diferente. La función gets lee todos los caracteres que hay hasta
encontrar un salto de lı́nea. Dichos caracteres, excepto el salto de lı́nea, se almacenan a partir
de la dirección de memoria que se indique como argumento y se añade un terminador.
Aquı́ tienes un ejemplo:
1 #include <stdio.h>
2
3 #define MAXLON 11
4
5 int main(void)
6 {
7 char a[MAXLON+1], b[MAXLON+1];
8
13 return 0;
14 }
Ejecutemos el programa:
Introduce una cadena: uno dos
Introduce otra cadena: tres cuatro
La primera es uno dos y la segunda es tres cuatro
Overflow exploit
El manejo de cadenas C es complicado. . . y peligroso. La posibilidad de que se almace-
nen más caracteres de los que caben en una zona de memoria reservada para una cadena
ha dado lugar a una técnica de cracking muy común: el overflow exploit (que significa
((aprovechamiento del desbordamiento))), también conocido por smash the stack (((machacar
la pila))).
Si un programa C lee una cadena con scanf o gets es vulnerable a este tipo de ataques.
La idea básica es la siguiente. Si c es una variable local a una función (en el siguiente capı́tulo
veremos cómo), reside en una zona de memoria especial: la pila. Podemos desbordar la zona
de memoria reservada para la cadena c escribiendo un texto más largo del que cabe en
ella. Cuando eso ocurre, estamos ocupando memoria en una zona de la pila que no nos
((pertenece)). Podemos conseguir ası́ escribir información en una zona de la pila reservada
a información como la dirección de retorno de la función. El exploit se basa en asignar
a la dirección de retorno el valor de una dirección en la que habremos escrito una rutina
especial en código máquina. ¿Y cómo conseguimos introducir una rutina en código máquina
en un programa ajeno? ¡En la propia cadena que provoca el desbordamiento, codificándola
en binario! La rutina de código máquina suele ser sencilla: efectúa una simple llamada al
sistema operativo para que ejecute un intérprete de órdenes Unix. El intérprete se ejecutará
con los mismos permisos que el programa que hemos reventado. Si el programa atacado
se ejecutaba con permisos de root, habremos conseguido ejecutar un intérprete de órdenes
como root. ¡El ordenador es nuestro!
¿Y cómo podemos proteger a nuestros programas de los overflow exploit? Pues, para
empezar, no utilizando nunca scanf o gets directamente. Como es posible leer de teclado
carácter a carácter (lo veremos en el capı́tulo dedicado a ficheros), podemos definir nuestra
propia función de lectura de cadenas: una función de lectura que controle que nunca se
escribe en una zona de memoria más información de la que cabe.
Dado que gets es tan vulnerable a los overflow exploit, el compilador de C te dará un
aviso cuando la uses. No te sorprendas, pues, cuando veas un mensaje como éste: ((the
‘gets’ function is dangerous and should not be used)).
• leer una lı́nea completa con gets (usa una avariable auxiliar para ello),
• y extraer de ella los valores escalares que se deseaba leer con ayuda de la función
sscanf .
La función sscanf es similar a scanf (fı́jate en la ((s)) inicial), pero no obtiene información
leyéndola del teclado, sino que la extrae de una cadena.
Un ejemplo ayudará a entender el procedimiento:
lecturas.c lecturas.c
1 #include <stdio.h>
2
3 #define MAXLINEA 80
4 #define MAXFRASE 40
5
6 int main(void)
7 {
8 int a, b;
9 char frase[MAXFRASE+1];
10 char linea[MAXLINEA+1];
11
24 return 0;
25 }
En el programa hemos definido una variable auxiliar, linea, que es una cadena con capacidad
para 80 caracteres más el terminador (puede resultar conveniente reservar más memoria para
ella en según qué aplicación). Cada vez que deseamos leer un valor escalar, leemos en linea un
texto que introduce el usuario y obtenemos el valor escalar con la función sscanf . Dicha función
recibe, como primer argumento, la cadena en linea; como segundo, una cadena con marcas
de formato; y como tercer parámetro, la dirección de la variable escalar en la que queremos
depositar el resultado de la lectura.
Es un proceso un tanto incómodo, pero al que tenemos que acostumbrarnos. . . de momento.
3 int main(void)
4 {
5 char original [MAXLON+1] = "cadena";
6 char copia[MAXLON+1];
7
8 copia = original ;
9
10 return 0;
11 }
3 int main(void)
4 {
5 char original [MAXLON+1] = "cadena";
6 char copia[MAXLON+1];
7 int i;
8
12 return 0;
13 }
Fı́jate en que el bucle recorre los 10 caracteres que realmente hay en original pero, de hecho,
sólo necesitas copiar los caracteres que hay hasta el terminador, incluyéndole a él.
1 #define MAXLON 10
2
3 int main(void)
4 {
5 char original [MAXLON+1] = "cadena";
6 char copia[MAXLON+1];
7 int i;
8
15 return 0;
16 }
0 1 2 3 4 5 6 7 8 9
original c a d e n a \0
0 1 2 3 4 5 6 7 8 9
copia c a d e n a \0
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 100 ¿Qué problema presenta esta otra versión del mismo programa?
1 #define MAXLON 10
2
3 int main(void)
4 {
5 char original [MAXLON+1] = "cadena";
6 char copia[MAXLON+1];
7 int i;
8
16 return 0;
17 }
.............................................................................................
Aún podemos hacerlo ((mejor)):
1 #define MAXLON 10
2
3 int main(void)
4 {
5 char original [MAXLON+1] = "cadena";
6 char copia[MAXLON+1];
7 int i;
8
13 return 0;
14 }
¿Ves? La condición del for controla si hemos llegado al terminador o no. Como el termi-
nado no llega a copiarse, lo añadimos tan pronto finaliza el bucle. Este tipo de bucles, aunque
perfectamente legales, pueden resultar desconcertantes.
El copiado de cadenas es una acción frecuente, ası́ que hay funciones predefinidas para ello,
accesibles incluyendo la cabecera string.h:
1 #include <string.h>
2
3 #define MAXLON 10
4
5 int main(void)
6 {
7 char original [MAXLON+1] = "cadena";
8 char copia[MAXLON+1];
9
12 return 0;
13 }
Ten cuidado: strcpy (abreviatura de ((string copy))) no comprueba si el destino de la copia tiene
capacidad suficiente para la cadena, ası́ que puede provocar un desbordamiento. La función
strcpy se limita a copiar carácter a carácter hasta llegar a un carácter nulo.
Tampoco está permitido asignar un literal de cadena a un vector de caracteres fuera de la
zona de declaración de variables. Es decir, este programa es incorrecto:
1 #define MAXLON 10
2
3 int main(void)
4 {
5 char a[MAXLON+1];
6
!
7 a = "cadena"; // Mal!
8
9 return 0;
10 }
Si deseas asignar un literal de cadena, tendrás que hacerlo con la ayuda de strcpy:
1 #define MAXLON 10
2
3 int main(void)
4 {
5 char original [MAXLON+1] = "cadena";
6 char copia[MAXLON+1];
7 int i;
8
9 i = 0;
10 while ( (copia[i] = original [i++]) != ’\0’) ;
11 copia[i] = ’\0’;
12
13 return 0;
14 }
El bucle está vacı́o y la condición del bucle while es un tanto extraña. Se aprovecha de
que la asignación es una operación que devuelve un valor, ası́ que lo puede comparar con el
terminador. Y no sólo eso: el avance de i se logra con un postincremento en el mismı́simo
acceso al elemento de original . Este tipo de retruécanos es muy habitual en los programas
C. Y es discutible que ası́ sea: los programas que hacen este tipo de cosas no tienen por
qué ser más rápidos y resultan más difı́ciles de entender (a menos que lleves mucho tiempo
programando en C).
Aquı́ tienes una versión con una condición del bucle while diferente:
i = 0;
while (copia[i] = original [i++]) ;
copia[i] = ’\0’;
1 #include <string.h>
2
3 #define MAXLON 10
4
5 int main(void)
6 {
7 char a[MAXLON+1];
8
9 strcpy(a, "cadena");
10
11 return 0;
12 }
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 101 Diseña un programa que lea una cadena y copie en otra una versión encriptada. La
encriptación convertirá cada letra (del alfabeto inglés) en la que le sigue en la tabla ASCII
(excepto en el caso de las letras ((z)) y ((Z)), que serán sustituidas por ((a)) y ((A)), respectivamente.)
No uses la función strcpy.
· 102 Diseña un programa que lea una cadena que posiblemente contenga letras mayúsculas
y copie en otra una versión de la misma cuyas letras sean todas minúsculas. No uses la función
strcpy.
· 103 Diseña un programa que lea una cadena que posiblemente contenga letras mayúsculas
y copie en otra una versión de la misma cuyas letras sean todas minúsculas. Usa la función
strcpy para obtener un duplicado de la cadena y, después, recorre la copia para ir sustituyendo
en ella las letras mayúsculas por sus correspondientes minúsculas.
.............................................................................................
1 #include <string.h>
2
3 #define MAXLON 10
4
5 int main(void)
6 {
7 char original [MAXLON+1] = "cadena";
8 char copia[MAXLON+1];
9
12 return 0;
13 }
Pero tampoco strncpy es perfecta. Si la cadena original tiene más caracteres de los que
puede almacenar la cadena destino, la copia es imperfecta: no acabará en ’\0’. De todos
modos, puedes encargarte tú mismo de terminar la cadena en el último carácter, por si
acaso:
1 #include <string.h>
2
3 #define MAXLON 10
4
5 int main(void)
6 {
7 char original [MAXLON+1] = "cadena";
8 char copia[MAXLON+1];
9
13 return 0;
14 }
3 #define MAXLON 80
4
5 int main(void)
6 {
7 char a[MAXLON+1];
8 int i;
9
17 return 0;
18 }
El estilo C
El programa que hemos presentado para calcular la longitud de una cadena es un programa
C correcto, pero no es ası́ como un programador C expresarı́a esa misma idea. ¡No hace
falta que el bucle incluya sentencia alguna!:
1 #include <stdio.h>
2
3 #define MAXLON 80
4
5 int main(void)
6 {
7 char a[MAXLON+1];
8 int i;
9
16 return 0;
17 }
i = 0;
while (a[i++]) ;
El bucle funciona correctamente porque el valor ’\0’ significa ((falso)) cuando se interpreta
como valor lógico. El bucle itera, pues, hasta llegar a un valor falso, es decir, a un terminador.
3 i = 1;
4 a[i] = i++;
Calcular la longitud de una cadena es una operación frecuentemente utilizada, ası́ que está
predefinida en la biblioteca de tratamiento de cadenas. Si incluı́mos la cabecera string.h,
podemos usar la función strlen (abreviatura de ((string length))):
while o for
Los bucles while pueden sustituirse muchas veces por bucles for equivalentes, bastante
más compactos:
1 #include <stdio.h>
2
3 #define MAXLON 80
4
5 int main(void)
6 {
7 char a[MAXLON+1];
8 int i;
9
15 return 0;
16 }
Todas las versiones del programa que hemos presentado son equivalentes. Escoger una
u otra es cuestión de estilo.
1 #include <stdio.h>
2 #include <string.h>
3
4 #define MAXLON 80
5
6 int main(void)
7 {
8 char a[MAXLON+1];
9 int l;
10
16 return 0;
17 }
Has de ser consciente de qué hace strlen: lo mismo que hacı́a el primer programa, es decir,
recorrer la cadena de izquierda a derecha incrementando un contador hasta llegar al terminador
nulo. Esto implica que tarde tanto más cuanto más larga sea la cadena. Has de estar al tanto,
pues, de la fuente de ineficiencia que puede suponer utilizar directamente strlen en lugares
crı́ticos como los bucles. Por ejemplo, esta función cuenta las vocales minúsculas de una cadena
leı́da por teclado:
1 #include <stdio.h>
2 #include <string.h>
3
4 #define MAXLON 80
5
6 int main(void)
7 {
8 char a[MAXLON+1];
9 int i, contador ;
10
19 return 0;
20 }
Pero tiene un problema de eficiencia. Con cada iteración del bucle for se llama a strlen y strlen
tarda un tiempo proporcional a la longitud de la cadena. Si la cadena tiene, pongamos, 60
caracteres, se llamará a strlen 60 veces para efectuar la comparación, y para cada llamada,
strlen tardará unos 60 pasos en devolver lo mismo: el valor 60. Esta nueva versión del mismo
programa no presenta ese inconveniente:
1 #include <stdio.h>
2 #include <string.h>
3
4 #define MAXLON 80
5
6 int main(void)
7 {
8 char a[MAXLON+1];
9 int i, longitud , contador ;
10
20 return 0;
21 }
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 104 Diseña un programa que lea una cadena y la invierta.
· 105 Diseña un programa que lea una palabra y determine si es o no es palı́ndromo.
· 106 Diseña un programa que lea una frase y determine si es o no es palı́ndromo. Recuerda
que los espacios en blanco y los signos de puntuación no se deben tener en cuenta a la hora de
determinar si la frase es palı́ndromo.
· 107 Escribe un programa C que lea dos cadenas y muestre el ı́ndice del carácter de la
primera cadena en el que empieza, por primera vez, la segunda cadena. Si la segunda cadena
no está contenida en la primera, el programa nos lo hará saber.
(Ejemplo: si la primera cadena es "un ejercicio de ejemplo" y la segunda es "eje", el
programa mostrará el valor 3.)
· 108 Escribe un programa C que lea dos cadenas y muestre el ı́ndice del carácter de la
primera cadena en el que empieza por última vez una aparición de la segunda cadena. Si la
segunda cadena no está contenida en la primera, el programa nos lo hará saber.
(Ejemplo: si la primera cadena es "un ejercicio de ejemplo" y la segunda es "eje", el
programa mostrará el valor 16.)
· 109 Escribe un programa que lea una lı́nea y haga una copia de ella eliminando los espacios
en blanco que haya al principio y al final de la misma.
· 110 Escribe un programa que lea repetidamente lı́neas con el nombre completo de una
persona. Para cada persona, guardará temporalmente en una cadena sus iniciales (las letras
con mayúsculas) separadas por puntos y espacios en blanco y mostrará el resultado en pantalla.
El programa finalizará cuando el usuario escriba una lı́nea en blanco.
· 111 Diseña un programa C que lea un entero n y una cadena a y muestre por pantalla el
valor (en base 10) de la cadena a si se interpreta como un número en base n. El valor de n debe
estar comprendido entre 2 y 16. Si la cadena a contiene un carácter que no corresponde a un
dı́gito en base n, notificará el error y no efectuará cálculo alguno.
Ejemplos:
si a es "ff" y n es 16, se mostrará el valor 255;
si a es "f0" y n es 15, se notificará un error: ((f no es un dı́gito en base 15));
si a es "1111" y n es 2, se mostrará el valor 15.
· 112 Diseña un programa C que lea una lı́nea y muestre por pantalla el número de palabras
que hay en ella.
.............................................................................................
2.2.6. Concatenación
Python permitı́a concatenar cadenas con el operador +. En C no puedes usar + para concatenar
cadenas. Una posibilidad es que las concatenes tú mismo ((a mano)), con bucles. Este programa,
por ejemplo, pide dos cadenas y concatena la segunda a la primera:
1 #include <stdio.h>
2
3 #define MAXLON 80
4
5 int main(void)
6 {
7 char a[MAXLON+1], b[MAXLON+1];
8 int longa, longb;
9 int i;
10
14 longa = strlen(a);
15 longb = strlen(b);
16 for (i=0; i<longb; i++)
17 a[longa+i] = b[i];
18 a[longa+longb] = ’\0’;
19 printf ("Concatenación de ambos: %s", a);
20
21 return 0;
22 }
Pero es mejor usar la función de librerı́a strcat (por ((string concatenate))):
1 #include <stdio.h>
2 #include <string.h>
3
4 #define MAXLON 80
5
6 int main(void)
7 {
8 char a[MAXLON+1], b[MAXLON+1];
9
17 return 0;
18 }
4 #define MAXLON 80
5
6 int main(void)
7 {
8 char a[MAXLON+1], b[MAXLON+1], c[MAXLON+1];
9
18 return 0;
19 }
Recuerda que es responsabilidad del programador asegurarse de que la cadena que recibe la
concatenación dispone de capacidad suficiente para almacenar la cadena resultante.
Por cierto, el operador de repetición de cadenas que encontrábamos en Python (operador
*) no está disponible en C ni hay función predefinida que lo proporcione.
Recuerda: los dos datos de strcat y strcpy han de ser cadenas y no es aceptable que uno
de ellos sea un carácter.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 113 Escribe un programa C que lea el nombre y los dos apellidos de una persona en tres
cadenas. A continuación, el programa formará una sóla cadena en la que aparezcan el nombre
y los apellidos separados por espacios en blanco.
· 114 Escribe un programa C que lea un verbo regular de la primera conjugación y lo mues-
tre por pantalla conjugado en presente de indicativo. Por ejemplo, si lee el texto programar,
mostrará por pantalla:
yo programo
tú programas
él programa
nosotros programamos
vosotros programáis
ellos programan
.............................................................................................
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 118 ¿Qué problema presenta este programa?
1 #include <stdio.h>
2 #include <ctype.h>
3
4 int main(void)
5 {
6 char b[2] = "a";
7
8 if (isalpha(b))
13 return 0;
14 }
.............................................................................................
3 #define MAXLON 80
4
5 int main(void)
6 {
7 char a[MAXLON+1] = "una";
8 char b[MAXLON+1] = "cadena";
9 char c[MAXLON+1];
10
14 return 0;
15 }
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 119 ¿Qué almacena en la cadena a la siguiente sentencia?
· 120 Escribe un programa que pida el nombre y los dos apellidos de una persona. Cada uno
de esos tres datos debe almacenarse en una variable independiente. A continuación, el programa
creará y mostrará una nueva cadena con los dos apellidos y el nombre (separado de los apellidos
por una coma). Por ejemplo, Juan Pérez López dará lugar a la cadena "Pérez López, Juan".
.............................................................................................
normaliza.c normaliza.c
1 #include <stdio.h>
2 #include <string.h>
3 #include <ctype.h>
4
5 #define MAXLON 80
6
7 int main(void)
8 {
9 char a[MAXLON+1], b[MAXLON+1];
10 int longitud , i, j;
11
23 return 0;
24 }
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 121 Modifica normaliza.c para que elimine, si los hay, los blancos inicial y final de la
cadena normalizada.
· 122 Haz un programa que lea una frase y construya una cadena que sólo contenga sus letras
minúsculas o mayúsculas en el mismo orden con que aparecen en la frase.
· 123 Haz un programa que lea una frase y construya una cadena que sólo contenga sus
letras minúsculas o mayúsculas en el mismo orden con que aparecen en la frase, pero sin repetir
ninguna.
· 124 Lee un texto por teclado (con un máximo de 1000 caracteres) y muestra por pantalla
la frecuencia de aparición de cada una de las letras del alfabeto (considera únicamente letras
del alfabeto inglés), sin distinguir entre letras mayúsculas y minúsculas (una aparición de la
letra e y otra de la letra E cuentan como dos ocurrencias de la letra e).
.............................................................................................
16 return 0;
17 }
996:
1000: a[0][0]
1004: a[0][1]
1008: a[0][2]
1012: a[1][0]
1016: a[1][1]
1020: a[1][2]
1024: a[2][0]
1028: a[2][1]
1032: a[2][2]
1036:
Cuando accedemos a un elemento a[i][j], C sabe a qué celda de memoria acceder sumando
a la dirección de a el valor (i*3+j)*4 (el 4 es el tamaño de un int y el 3 e sel número de
columnas).
Aun siendo conscientes de cómo representa C la memoria, nosotros trabajaremos con una
representación de una matriz de 3 × 3 como ésta:
0 1 2
0
a
1
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 125 Este programa es incorrecto. ¿Por qué? Aun siendo incorrecto, produce cierta salida
por pantalla. ¿Qué muestra?
3 #define TALLA 3
4
5 int main(void)
6 {
7 int a[TALLA][TALLA];
8 int i, j;
9
17 return 0;
18 }
.............................................................................................
matrices.c matrices.c
1 #include <stdio.h>
2
3 #define TALLA 3
4
5 int main(void)
6 {
7 float a[TALLA][TALLA], b[TALLA][TALLA];
8 float s[TALLA][TALLA], p[TALLA][TALLA];
9 int i, j, k;
10
11 /* Lectura de la matriz a */
12 for (i=0; i<TALLA; i++)
13 for (j=0; j<TALLA; j++) {
14 printf ("Elemento (%d, %d): ", i, j); scanf ("%f", &a[i][j]);
15 }
16
17 /* Lectura de la matriz b */
18 for (i=0; i<TALLA; i++)
19 for (j=0; j<TALLA; j++) {
20 printf ("Elemento (%d, %d): ", i, j); scanf ("%f", &b[i][j]);
21 }
22
23 /* Cálculo de la suma */
24 for (i=0; i<TALLA; i++)
25 for (j=0; j<TALLA; j++)
26 s[i][j] = a[i][j] + b[i][j];
27
52 return 0;
53 }
Aún no sabemos definir nuestras propias funciones. En el próximo capı́tulo volveremos a ver
este programa y lo modificaremos para que use funciones definidas por nosotros.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 126 En una estación meteorológica registramos la temperatura (en grados centı́grados) cada
hora durante una semana. Almacenamos el resultado en una matriz de 7 × 24 (cada fila de la
matriz contiene las 24 mediciones de un dı́a). Diseña un programa que lea los datos por teclado
y muestre:
La máxima y mı́nima temperaturas de la semana.
La máxima y mı́nima temperaturas de cada dı́a.
La temperatura media de la semana.
La temperatura media de cada dı́a.
El número de dı́as en los que la temperatura media fue superior a 30 grados.
· 127 Representamos diez ciudades con números del 0 al 9. Cuando hay carretera que une
directamente a dos ciudades i y j, almacenamos su distancia en kilómetros en la celda d[i][j]
de una matriz de 10 × 10 enteros. Si no hay carretera entre ambas ciudades, el valor almacenado
en su celda de d es cero. Nos suministran un vector en el que se describe un trayecto que pasa
por las 10 ciudades. Determina si se trata de un trayecto válido (las dos ciudades de todo
par consecutivo están unidas por un tramo de carretera) y, en tal caso, devuelve el número de
kilómetros del trayecto. Si el trayecto no es válido, indı́calo con un mensaje por pantalla.
La matriz de distancias deberás inicializarla explı́citamente al declararla. El vector con el
recorrido de ciudades deberás leerlo de teclado.
· 128 Diseña un programa que lea los elementos de una matriz de 4 × 5 flotantes y genere
un vector de talla 4 en el que cada elemento contenga el sumatorio de los elementos de cada
fila. El programa debe mostrar la matriz original y el vector en este formato (evidentemente,
los valores deben ser los que correspondan a lo introducido por el usuario):
0 1 2 3 4 Suma
0 [ +27.33 +22.22 +10.00 +0.00 -22.22] -> +37.33
1 [ +5.00 +0.00 -1.50 +2.50 +10.00] -> +16.00
2 [ +3.45 +2.33 -4.56 +12.56 +12.01] -> +25.79
3 [ +1.02 +2.22 +12.70 +34.00 +12.00] -> +61.94
4 [ -2.00 -56.20 +3.30 +2.00 +1.00] -> -51.90
.............................................................................................
El programa que hemos presentado adolece de un serio inconveniente si nuestro objetivo era
construir un programa ((general)) para multiplicar matrices: sólo puede trabajar con matrices de
TALLA × TALLA, o sea, de 3 × 3. ¿Y si quisiéramos trabajar con matrices de tamaños arbitrarios?
El primer problema al que nos enfrentarı́amos es el de que las matrices han de tener una talla
máxima: no podemos, con lo que sabemos por ahora, reservar un espacio de memoria para las
matrices que dependa de datos que nos suministra el usuario en tiempo de ejecución. Usaremos,
pues, una constante MAXTALLA con un valor razonablemente grande: pongamos 10. Ello permitirá
trabajar con matrices con un número de filas y columnas menor o igual que 10, aunque será a
costa de malgastar memoria.
matrices.c
1 #include <stdio.h>
2
3 #define MAXTALLA 10
4
5 int main(void)
6 {
7 float a[MAXTALLA][MAXTALLA], b[MAXTALLA][MAXTALLA];
8 float s[MAXTALLA][MAXTALLA], p[MAXTALLA][MAXTALLA];
9 ...
columnas a 3
0 1 2 3 4 5 6 7 8 9
0
a
1
filas a 5 5
3 #define MAXTALLA 10
4
5 int main(void)
6 {
7 float a[MAXTALLA][MAXTALLA], b[MAXTALLA][MAXTALLA];
8 float s[MAXTALLA][MAXTALLA], p[MAXTALLA][MAXTALLA];
9 int filas_a, columnas_a, filas_b, columnas_b ;
10 int i, j, k;
11
12 /* Lectura de la matriz a */
13 printf ("Filas de a : "); scanf ("%d", &filas_a );
matrices.c
1 #include <stdio.h>
2
3 #define MAXTALLA 10
4
5 int main(void)
6 {
7 float a[MAXTALLA][MAXTALLA], b[MAXTALLA][MAXTALLA];
8 float s[MAXTALLA][MAXTALLA], p[MAXTALLA][MAXTALLA];
9 int filas_a, columnas_a, filas_b, columnas_b;
10 int filas_s, columnas_s ;
11 int i, j, k;
12
13 /* Lectura de la matriz a */
14 printf ("Filas de a : "); scanf ("%d", &filas_a);
15 printf ("Columnas de a: "); scanf ("%d", &columnas_a);
16 for (i=0; i<filas_a; i++)
17 for (j=0; j<columnas_a; j++) {
18 printf ("Elemento (%d, %d): ", i, j); scanf ("%f", &a[i][j]);
19 }
20
21 /* Lectura de la matriz b */
22 ...
23
24 /* Cálculo de la suma */
25 if (filas_a == filas_b && columnas_a == columnas_b) {
26 filas_s = filas_a;
27 columnas_s = columnas_a;
28 for (i=0; i<filas_s; i++)
29 for (j=0; j<filas_s; j++)
30 s[i][j] = a[i][j] + b[i][j];
31 }
32
45 ...
3 #define MAXTALLA 10
4
5 int main(void)
6 {
7 float a[MAXTALLA][MAXTALLA], b[MAXTALLA][MAXTALLA];
8 float s[MAXTALLA][MAXTALLA], p[MAXTALLA][MAXTALLA];
9 int filas_a, columnas_a, filas_b, columnas_b;
10 int filas_s, columnas_s, filas_p, columnas_p ;
11 int i, j, k;
12
13 /* Lectura de la matriz a */
14 printf ("Filas de a : "); scanf ("%d", &filas_a);
15 printf ("Columnas de a: "); scanf ("%d", &columnas_a);
16 for (i=0; i<filas_a; i++)
17 for (j=0; j<columnas_a; j++) {
18 printf ("Elemento (%d, %d): ", i, j); scanf ("%f", &a[i][j]);
19 }
20
21 /* Lectura de la matriz b */
22 printf ("Filas de a : "); scanf ("%d", &filas_b);
23 printf ("Columnas de a: "); scanf ("%d", &columnas_b);
24 for (i=0; i<filas_b; i++)
25 for (j=0; j<columnas_b; j++) {
26 printf ("Elemento (%d, %d): ", i, j); scanf ("%f", &b[i][j]);
27 }
28
29 /* Cálculo de la suma */
30 if (filas_a == filas_b && columnas_a == columnas_b) {
31 filas_s = filas_a;
32 columnas_s = columnas_a;
33 for (i=0; i<filas_s; i++)
34 for (j=0; j<filas_s; j++)
35 s[i][j] = a[i][j] + b[i][j];
36 }
37
74 return 0;
75 }
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 129 Extiende el programa de calculadora matricial para efectuar las siguientes operaciones:
Producto de una matriz por un escalar. (La matriz resultante tiene la misma dimensión que
la original y cada elemento se obtiene multiplicando el escalar por la celda correspondiente
de la matriz original.)
Transpuesta de una matriz. (La transpuesta de una matriz de n × m es una matriz de
m × n en la que el elemento de la fila i y columna j tiene el mismo valor que el que ocupa
la celda de la fila j y columna i en la matriz original.)
· 130 Una matriz tiene un valle si el valor de una de sus celdas es menor que el de cualquiera
de sus 8 celdas vecinas. Diseña un programa que lea una matriz (el usuario te indicará de
cuántas filas y columnas) y nos diga si la matriz tiene un valle o no. En caso afirmativo, nos
mostrará en pantalla las coordenadas de todos los valles, sus valores y el de sus celdas vecinas.
La matriz debe tener un número de filas y columnas mayor o igual que 3 y menor o igual
que 10. Las casillas que no tienen 8 vecinos no se consideran candidatas a ser valle (pues no
tienen 8 vecinos).
Aquı́ tienes un ejemplo de la salida esperada para esta matriz de 4 × 5:
1 2 9 5 5
3 2 9 4 5
6 1 8 7 6
6 3 8 0 9
(Observa que al usuario se le muestran filas y columnas numeradas desde 1, y no desde 0.)
· 131 Modifica el programa del ejercicio anterior para que considere candidato a valle a
cualquier celda de la matriz. Si una celda tiene menos de 8 vecinos, se considera que la celda
es valle si su valor es menor que el de todos ellos.
Para la misma matriz del ejemplo del ejercicio anterior se obtendrı́a esta salida:
Valle en fila 1 columna 1:
x x x
x 1 2
x 3 2
Valle en fila 2 columna 4:
9 5 5
9 4 5
8 7 6
Valle en fila 3 columna 2:
3 2 9
6 1 8
6 3 8
Valle en fila 4 columna 4:
8 7 6
8 0 9
x x x
.............................................................................................
char v[10][MAXLON+1];
Cada fila de la matriz es una cadena y, como tal, debe terminar en un carácter nulo.
Este fragmento declara e inicializa un vector de tres cadenas:
#define MAXLON 80
3 #define MAXLON 81
4
5 int main(void)
6 {
7 char v[3][MAXLON+1];
8 int i;
9
16 return 0;
17 }
Vamos a desarrollar un programa útil que hace uso de un vector de caracteres: un pequeño
corrector ortográfico para inglés. El programa dispondrá de una lista de palabras en inglés (que
encontrarás en la página web de la asignatura, en el fichero ingles.h), solicitará al usuario que
introduzca por teclado un texto en inglés y le informará de qué palabras considera erróneas por
no estar incluı́das en su diccionario. Aquı́ tienes un ejemplo de uso del programa:
Introduce una frase: does this sentence contiene only correct words, eh?
palabra no encontrada: contiene
palabra no encontrada: eh
El fichero ingles.h es una cabecera de la que te mostramos ahora las primeras y últimas
lı́neas:
ingles.h ingles.h
1 #define DICCPALS 45378
2 #define MAXLONPAL 28
3 char diccionario[DICCPALS][MAXLONPAL+1] = {
4 "aarhus",
5 "aaron",
6 "ababa",
7 "aback",
8 "abaft",
9 "abandon",
10 "abandoned",
11 "abandoning",
12 "abandonment",
.
.
.
45376 "zorn",
45377 "zoroaster",
45378 "zoroastrian",
45379 "zulu",
45380 "zulus",
45381 "zurich"
45382 };
corrector.c
1 #include <stdio.h>
2 #include "ingles.h"
Fı́jate en que incluı́mos el fichero ingles.h encerrando su nombre entre comillas dobles, y no
entre < y >. Hemos de hacerlo ası́ porque ingles.h es una cabecera nuestra y no reside en los
directorios estándar del sistema (más sobre esto en el siguiente capı́tulo).
El programa empieza solicitando una cadena con gets. A continuación, la dividirá en un
nuevo vector de palabras. Supondremos que una frase no contiene más de 100 palabras y que
una palabra es una secuencia cualquiera de letras. Si el usuario introduce más de 100 palabras,
le advertiremos de que el programa sólo corrige las 100 primeras. Una vez formada la lista
de palabras de la frase, el programa buscará cada una de ellas en el diccionario. Las que no
estén, se mostrarán en pantalla precedidas del mensaje: palabra no encontrada. Vamos allá:
empezaremos por la lectura de la frase y su descomposición en una lista de palabras.
10 int main(void)
11 {
12 char frase[MAXLONFRASE+1];
13 char palabra[MAXPALSFRASE][MAXLONPALFRASE+1];
14 int palabras; // Número de palabras en la frase
15 int lonfrase, i, j;
16
17 /* Lectura de la frase */
18 printf ("Introduce una frase: ");
19 gets(frase);
20
21 lonfrase = strlen(frase);
22
27 palabras = 0;
28 while (i<lonfrase) { // Recorrer todos los caracteres
29
40 // Saltarse las no-letras que separan esta palabra de la siguiente (si las hay).
41 while (i<lonfrase && !isalpha(frase[i])) i++;
42 }
43
48 return 0;
49 }
¡Buf! Complicado, ¿no? ¡Ya estamos echando en falta el método split de Python! No nos viene
mal probar si nuestro código funciona mostrando las palabras que ha encontrado en la frase.
Por eso hemos añadido las lı́neas 45–47. Una vez hayas ejecutado el programa y comprobado
que funciona correctamente hasta este punto, comenta el bucle que muestra las palabras:
45 /* Comprobación de posibles errores */
46 // for (i=0; i<palabras; i++)
47 // printf ("%s\n", palabra[i]);
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 132 Un programador, al copiar el programa, ha sustituido la lı́nea que reza ası́:
while (i<lonfrase && !isalpha(frase[i])) i++; // Saltarse las no-letras iniciales.
48
?
49 /* Están todas las palabras en el diccionario? */
50 for (i=0; i<palabras; i++) {
51 encontrada = 0;
52 for (j=0; j<DICCPALS; j++)
?
53 if (strcmp(palabra[i],diccionario[j]) == 0) { // Es palabra[i] igual que diccionario[j]?
54 encontrada = 1;
55 break;
56 }
57 if (!encontrada)
58 printf ("palabra no encontrada: %s\n", palabra[i]);
59 }
60 return 0;
61 }
Ten en cuenta lo que hace strcmp: recorre las dos cadenas hasta encontrar alguna diferencia
entre ellas o concluir que son idénticas. Es, por tanto, una operación bastante costosa en tiempo.
¿Podemos reducir el número de comparaciones? ¡Claro! Como el diccionario está ordenado al-
fabéticamente, podemos abortar el recorrido cuando llegamos a una voz del diccionario posterior
(según el orden alfabético) a la que buscamos:
corrector 3.c corrector.c
.
.
.
48
?
49 /* Están todas las palabras en el diccionario? */
50 for (i=0; i<palabras; i++) {
51 encontrada = 0;
52 for (j=0; j<DICCPALS; j++)
?
53 if (strcmp(palabra[i],diccionario[j]) == 0) { // Es palabra[i] igual que diccionario[j]?
54 encontrada = 1;
55 break;
56 }
?
57 else if (strcmp(palabra[i], diccionario[j]) < 0) // palabra[i])) < diccionario[j]?
58 break;
59 if (!encontrada)
60 printf ("palabra no encontrada: %s\n", palabra[i]);
61 }
62 return 0;
63 }
Con esta mejora hemos intentado reducir a la mitad el número de comparaciones con cadenas
del diccionario, pero no hemos logrado nuestro objetivo: ¡aunque, en promedio, efectuamos
comparaciones con la mitad de las palabras del diccionario, estamos llamando dos veces a
strcmp! Es mejor almacenar el resultado de una sola llamada a strcmp en una variable:
corrector 4.c corrector.c
.
.
.
48
?
49 /* Están todas las palabras en el diccionario? */
50 for (i=0; i<palabras; i++) {
51 encontrada = 0;
52 for (j=0; j<DICCPALS; j++) {
53 comparacion = strcmp(palabra[i], diccionario[j]) ;
?
54 if ( comparacion == 0 ) { // Es palabra[i] igual que diccionario[j]?
55 encontrada = 1; break;
56 }
?
57 else if ( comparacion < 0 ) // Es palabra[i] menor que diccionario[j]?
58 break;
59 }
60 if (!encontrada)
61 printf ("palabra no encontrada: %s\n", palabra[i]);
62 }
63 return 0;
64 }
corrector.c
.
.
.
97
?
98 /* Están todas las palabras en el diccionario? */
99 for (i=0; i<palabras; i++) {
100 encontrada = 0;
101 izquierda = 0;
102 derecha = DICCPALS;
103
117 if (!encontrada)
118 printf ("palabra no encontrada: %s\n", palabra[i]);
119 }
120
121 return 0;
122 }
· 138 Representamos la baraja de cartas con un vector de cadenas. Los palos son "oros ",
"copas", "espadas" y "bastos". Las cartas con números entre 2 y 9 se describen con el texto
"número de palo" (ejemplo: "2 de oros", "6 de copas"). Los ases se describen con la cadena
"as de palo", las sotas con "sota de palo", los caballos con "caballo de palo" y los reyes
con "rey de palo".
Escribe un programa que genere la descripción de las 40 cartas de la baraja. Usa bucles
siempre que puedas y compón las diferentes partes de cada descripción con strcat o sprintf . A
continuación, baraja las cartas utilizando para ello el generador de números aleatorios y muestra
el resultado por pantalla.
· 139 Diseña un programa de ayuda al diagnóstico de enfermedades. En nuestra base de
datos hemos registrado 10 enfermedades y 10 sı́ntomas:
El programa leerá una lı́nea y mostrará por pantalla su traducción a código Morse. Ten en
cuenta que las letras se deben separar por pausas (un espacio blanco) y las palabras por pausas
largas (tres espacios blancos). Los acentos no se tendrán en cuenta al efectuar la traducción (la
letra Á, por ejemplo, se representará con .-) y la letra ’~
N’ se mostrará como una ’N’. Los signos
que no aparecen en la tabla (comas, admiraciones, etc.) no se traducirán, excepción hecha del
punto, que se traduce por la palabra STOP. Te conviene pasar la cadena a mayúsculas (o efectuar
esta transformación sobre la marcha), pues la tabla Morse sólo recoge las letras mayúsculas y
los dı́gitos.
Por ejemplo, la cadena "Hola, mundo." se traducirá por
Debes usar un vector de cadenas para representar la tabla de traducción a Morse. El código
Morse de la letra ’A’, por ejemplo, estará accesible como una cadena en morse[’A’].
(Tal vez te sorprenda la notación morse[’A’]. Recuerda que ’A’ es el número 65, pues
el carácter ’A’ tiene ese valor ASCII. Ası́ pues, morse[’A’] y morse[65] son lo mismo. Por
cierto: el vector de cadenas morse sólo tendrá códigos para las letras mayúsculas y los dı́gitos;
recuerda inicializar el resto de componentes con la cadena vacı́a.)
· 142 Escribe un programa que lea un texto escrito en código Morse y lo traduzca al código
alfabético.
Si, por ejemplo, el programa lee por teclado esta cadena:
".... --- .-.. .- -- ..- -. -.. --- ... - --- .--."
mostrará en pantalla el texto HOLAMUNDOSTOP.
.............................................................................................
2.4. Registros
Los vectores permiten agrupar varios elementos de un mismo tipo. Cada elemento de un vector
es accesible a través de un ı́ndice.
En ocasiones necesitarás agrupar datos de diferentes tipos y/o preferirás acceder a diferentes
elementos de un grupo de datos a través de un identificador, no de un ı́ndice. Los registros
son agrupaciones heterogéneas de datos cuyos elementos (denominados campos) son accesibles
mediante identificadores. Ya hemos estudiado registros en Python, ası́ que el concepto y su
utilidad han de resultarte familiares.
Veamos ahora un diseño tı́pico de registro. Supongamos que deseamos mantener los siguien-
tes datos de una persona:
su nombre (con un máximo de 40 caracteres),
su edad (un entero),
su DNI (una cadena de 9 caracteres).
Podemos definir un registro ((persona)) antes de la aparición de main:
#define MAXNOM 40
#define LONDNI 9
struct Persona {
char nombre[MAXNOM+1];
int edad ;
char dni[LONDNI+1];
}; // <- Fı́jate en el punto y coma: es fácil olvidarse de ponerlo.
En tu programa puedes acceder a cada uno de los campos de una variable de tipo struct
separando con un punto el identificador de la variable del correspondiente identificador del
campo. Por ejemplo, pepe.edad es la edad de Pepe (un entero sin signo que ocupa un byte),
juan.nombre es el nombre de Juan (una cadena), y ana.dni [9] es la letra del DNI de Ana (un
carácter).
Cada variable de tipo struct Persona ocupa, en principio, 55 bytes: 41 por el nombre, 4
por la edad y 10 por el DNI. (Si quieres saber por qué hemos resaltado lo de ((en principio)), lee
el cuadro ((Alineamientos)).)
Este programa ilustra cómo acceder a los campos de un registro leyendo por teclado sus
valores y mostrando por pantalla diferentes informaciones almacenadas en él:
registro.c
1 #include <stdio.h>
2 #include <string.h>
3
4 #define MAXNOM 40
5 #define LONDNI 9
6
7 struct Persona {
8 char nombre[MAXNOM+1];
Alineamientos
El operador sizeof devuelve el tamaño en bytes de un tipo o variable. Analiza este programa:
alineamiento.c alineamiento.c
1 #include <stdio.h>
2
3 struct Registro {
4 char a;
5 int b;
6 };
7
8 int main(void)
9 {
10 printf ("Ocupación: %d bytes\n", sizeof (struct Registro));
11 return 0;
12 }
Parece que vaya a mostrar en pantalla el mensaje ((Ocupación: 5 bytes)), pues un char
ocupa 1 byte y un int ocupa 4. Pero no es ası́:
Ocupación: 8 bytes
La razón de que ocupe más de lo previsto es la eficiencia. Los ordenadores con arqui-
tectura de 32 bits agrupan la información en bloques de 4 bytes. Cada uno de esos bloques
se denomina ((palabra)). Cada acceso a memoria permite traer al procesador los 4 bytes de
una palabra. Si un dato está a caballo entre dos palabras, requiere dos accesos a memoria,
afectando seriamente a la eficiencia del programa. El compilador trata de generar un pro-
grama eficiente y da prioridad a la velocidad de ejecución frente al consumo de memoria. En
nuestro caso, esta prioridad se ha traducido en que el segundo campo se almacene en una
palabra completa, aunque ello suponga desperdiciar 3 bytes en el primero de los campos.
9 int edad ;
10 char dni[LONDNI+1];
11 };
12
13 int main(void)
14 {
15 struct Persona ejemplo;
16 char linea[81];
17 int i, longitud ;
18
41 return 0;
42 }
Los registros pueden copiarse ı́ntegramente sin mayor problema. Este programa, por ejemplo,
copia el contenido de un registro en otro y pasa a minúsculas el nombre de la copia:
5 #define MAXNOM 40
6 #define LONDNI 9
7
8 struct Persona {
9 char nombre[MAXNOM+1];
10 int edad ;
11 char dni[LONDNI+1];
12 };
13
14 int main(void)
15 {
16 struct Persona una, copia;
17 char linea[81];
18 int i, longitud ;
19
26 longitud = strlen(copia.nombre);
27 for (i=0; i<longitud ; i++)
28 copia.nombre[i] = tolower (copia.nombre[i]);
29
38 return 0;
39 }
Observa que la copia se efectúa incluso cuando los elementos del registro son vectores. O
sea, copiar vectores con una mera asignación está prohibido, pero copiar registros es posible.
Un poco incoherente, ¿no?
Por otra parte, no puedes comparar registros. Este programa, por ejemplo, efectúa una copia
de un registro en otro para, a continuación, intentar decirnos si ambos son iguales o no:
3 #define MAXNOM 40
4 #define LONDNI 9
5
6 struct Persona {
7 char nombre[MAXNOM+1];
8 int edad ;
9 char dni[LONDNI+1];
10 };
11
12 int main(void)
13 {
14 struct Persona una, copia;
15 char linea[81];
16 int i, longitud ;
17
29 return 0;
30 }
Pero ni siquiera es posible compilarlo. La lı́nea 24 contiene un error que el compilador señala
como ((invalid operands to binary ==)), o sea, ((operandos inválidos para la operación bina-
ria ==)). Entonces, ¿cómo podemos decidir si dos registros son iguales? Comparando la igualdad
de cada uno de los campos de un registro con el correspondiente campo del otro:
3 #define MAXNOM 40
4 #define LONDNI 9
5
6 struct Persona {
7 char nombre[MAXNOM+1];
8 int edad ;
9 char dni[LONDNI+1];
10 };
11
12 int main(void)
13 {
14 struct Persona una, copia;
15 char linea[81];
16 int i, longitud ;
17
30 return 0;
31 }
struct Persona {
char nombre[10];
char apellido[10];
};
Cada dato de tipo struct Persona ocupa 20 bytes. Si una persona a tiene su campo
a.nombre con valor "Pepe", sólo los cinco primeros bytes de su nombre tienen un valor
bien definido. Los cinco siguientes pueden tener cualquier valor aleatorio. Otro registro b
cuyo campo b.nombre también valga "Pepe" (y tenga idéntico apellido) puede tener valores
diferentes en su segundo grupo de cinco bytes. Una comparación ((bit a bit)) nos dirı́a que
los registros son diferentes.
La asignación no entraña este tipo de problema, pues la copia es ((bit a bit)). Como
mucho, resulta algo ineficiente, pues copiará hasta los bytes de valor indefinido.
struct Algo {
int x;
char nombre[10];
float y;
};
...
Los vectores estáticos tienen una talla fija. Cuando necesitamos un vector cuya talla varı́a o no
se conoce hasta iniciada la ejecución del programa usamos un truco: definimos un vector cuya
talla sea suficientemente grande para la tarea que vamos a abordar y mantenemos la ((talla real))
en una variable. Lo hemos hecho con el programa que calcula algunas estadı́sticas con una serie
de edades: definı́amos un vector edad con capacidad para almacenar la edad de MAX_PERSONAS
y una variable personas, cuyo valor siempre era menor o igual que MAX_PERSONAS, nos indicaba
cuántos elementos del vector contenı́an realmente datos. Hay algo poco elegante en esa solución:
las variables edad y personas son variables independientes, que no están relacionadas entre sı́
en el programa (salvo por el hecho de que nosotros sabemos que sı́ lo están). Una solución más
elegante pasa por crear un registro que contenga el número de personas y, en un vector, las
edades. He aquı́ el programa que ya te presentamos en su momento convenientemente modificado
según este nuevo principio de diseño:
4 #define MAX_PERSONAS 20
5
6 struct ListaEdades {
7 int edad [MAX_PERSONAS]; // Vector con capacidad para MAX PERSONAS edades.
8 int talla; // Número de edades realmente almacenadas.
9 };
10
11 int main(void)
12 {
13 struct ListaEdades personas;
14 int i, j, aux , suma_edad ;
15 float media, desviacion, suma_desviacion;
16 int moda, frecuencia, frecuencia_moda, mediana;
17
18 /* Lectura de edades */
19 personas.talla = 0;
20 do {
21 printf ("Introduce edad de la persona %d (si es negativa, acabar): ",
22 personas.talla +1);
23 scanf ("%d", &personas.edad [ personas.talla ]);
24 personas.talla ++;
25 } while ( personas.talla < MAX_PERSONAS && personas.edad [ personas.talla -1] >= 0);
26 personas.talla --;
27
28 if ( personas.talla > 0) {
29 /* Cálculo de la media */
30 suma_edad = 0;
31 for (i=0; i< personas.talla ; i++)
32 suma_edad += personas.edad [i] ;
33 media = suma_edad / personas.talla ;
34
41 /* Cálculo de la moda */
42 for (i=0; i< personas.talla -1; i++) // Ordenación mediante burbuja.
43 for (j=0; j< personas.talla -i; j++)
44 if ( personas.edad [j] > personas.edad [j+1] ) {
45 aux = personas.edad [j] ;
46 personas.edad [j] = personas.edad [j+1] ;
47 personas.edad [j+1] = aux ;
48 }
49
50 frecuencia = 0;
51 frecuencia_moda = 0;
52 moda = -1;
53 for (i=0; i< personas.talla -1; i++)
54 if ( personas.edad [i] == personas.edad [i+1] )
55 if (++frecuencia > frecuencia_moda) {
56 frecuencia_moda = frecuencia;
57 moda = personas.edad [i] ;
58 }
59 else
60 frecuencia = 0;
61
62 /* Cálculo de la mediana */
65 /* Impresión de resultados */
66 printf ("Edad media : %f\n", media);
67 printf ("Desv. tı́pica: %f\n", desviacion);
68 printf ("Moda : %d\n", moda);
69 printf ("Mediana : %d\n", mediana);
70 }
71 else
72 printf ("No se introdujo dato alguno.\n");
73
74 return 0;
75 }
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 143 Modifica el programa de cálculo con polinomios que sirvió de ejemplo en el apar-
tado 2.1.5 para representar los polinomios mediante registros. Cada registro contendrá dos
campos: el grado del polinomio y el vector con los coeficientes.
.............................................................................................
y = mx + b
Las fórmulas asustan un poco, pero no contienen más que sumatorios. El programa que vamos
a escribir lee una serie de puntos (con un número máximo de, pongamos, 1000), y muestra los
valores de m y b.
Modelaremos los puntos con un registro:
struct Punto {
float x, y;
};
Pero 1000 es el número máximo de puntos. El número de puntos disponibles efectivamente será
menor o igual y su valor deberá estar accesible en alguna variable. Olvidémonos del vector p:
nos conviene definir un registro en el que se almacenen vector y talla real del vector.
struct ListaPuntos {
struct Punto punto[TALLAMAX];
int talla;
};
5 struct Punto {
6 float x, y;
7 };
8
9 struct ListaPuntos {
10 struct Punto punto[TALLAMAX];
11 int talla;
12 };
13
14 int main(void)
15 {
16 struct ListaPuntos lista ;
17 ...
Ahora que tenemos más claro cómo hemos modelado la información, vamos a resolver el
problema propuesto. Cada uno de los sumatorios se precalculará cuando se hayan leı́do los
puntos. De ese modo, simplificaremos significativamente las expresiones de cálculo de m y b.
Debes tener en cuenta que, aunque en las fórmulas se numeran los puntos empezando en 1, en
C se empieza en 0.
Veamos el programa completo:
ajuste.c ajuste.c
1 #include <stdio.h>
2
5 struct Punto {
6 float x, y;
7 };
9 struct ListaPuntos {
10 struct Punto punto[TALLAMAX];
11 int talla;
12 };
13
14 int main(void)
15 {
16 struct ListaPuntos lista;
17
22 /* Lectura de puntos */
23 printf ("Puntos a leer: "); scanf ("%d", &lista.talla);
24 for (i=0; i<lista.talla; i++) {
25 printf ("Coordenada x del punto %d: ", i); scanf ("%f", &lista.punto[i].x);
26 printf ("Coordenada y del punto %d: ", i); scanf ("%f", &lista.punto[i].y);
27 }
28
34 sy = 0.0;
35 for (i=0; i<lista.talla; i++)
36 sy += lista.punto[i].y;
37
38 sxy = 0.0;
39 for (i=0; i<lista.talla; i++)
40 sxy += lista.punto[i].x * lista.punto[i].y;
41
42 sxx = 0.0;
43 for (i=0; i<lista.talla; i++)
44 sxx += lista.punto[i].x * lista.punto[i].x;
45
56 return 0;
57 }
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 144 Diseña un programa que lea una lista de hasta 1000 puntos por teclado y los almacene
en una variable (del tipo que tú mismo definas) llamada representantes. A continuación, irá
leyendo nuevos puntos hasta que se introduzca el punto de coordenadas (0, 0). Para cada nuevo
punto, debes encontrar cuál es el punto más próximo de los almacenados en representantes.
Calcula la distancia entre dos puntos como la distancia euclı́dea.
· 145 Deseamos efectuar cálculos con enteros positivos de hasta 1000 cifras, más de las que
puede almacenar un int (o incluso long long int). Define un registro que permita representar
números de hasta 1000 cifras con un vector en el que cada elemento es una cifra (representada
con un char). Representa el número de cifras que tiene realmente el valor almacenado con un
campo del registro. Escribe un programa que use dos variables del nuevo tipo para leer dos
números y que calcule el valor de la suma y la resta de estos (supondremos que la resta siempre
proporciona un entero positivo como resultado).
.............................................................................................
struct Tiempo {
int minutos;
int segundos;
};
struct CompactDisc {
char titulo[LONTITULO+1];
char interprete[LONINTERPRETE+1];
struct Tiempo duracion;
int anyo;
};
Vamos a usar un vector para almacenar la colección, definiremos un máximo número de CDs:
1000. Eso no significa que la colección tenga 1000 discos, sino que puede tener a lo sumo 1000.
¿Y cuántos tiene en cada instante? Utilizaremos una variable para mantener el número de CDs
presente en la colección. Mejor aún: definiremos un nuevo tipo de registro que represente a la
colección entera de CDs. El nuevo tipo contendrá dos campos:
el vector de discos (con capacidad limitada a 1000 unidades),
y el número de discos en el vector.
He aquı́ la definición de la estructura y la declaración de la colección de CDs:
#define MAXDISCOS 1000
...
struct Coleccion {
struct CompactDisc cd [MAXDISCOS];
int cantidad ;
};
4 #define MAXLINEA 80
5
12 struct Tiempo {
13 int minutos;
14 int segundos;
15 };
16
17 struct CompactDisc {
18 char titulo[LONTITULO+1];
19 char interprete[LONINTERPRETE+1];
20 struct Tiempo duracion;
21 int anyo;
22 };
23
24 struct Coleccion {
25 struct CompactDisc cd [MAXDISCOS];
26 int cantidad ;
27 };
28
29 int main(void)
30 {
31 struct Coleccion mis_cds;
32 int opcion, i, j;
33 char titulo[LONTITULO+1], interprete[LONINTERPRETE+1];
34 char linea[MAXLINEA]; // Para evitar los problemas de scanf.
35
36 /* Inicialización de la colección. */
37 mis_cds.cantidad = 0;
38
55 switch(opcion) {
56 case Anyadir : // Añadir un CD.
57 if (mis_cds.cantidad == MAXDISCOS)
112 return 0;
113 }
En nuestro programa hemos separado la definición del tipo struct Coleccion de la declara-
ción de la variable mis_cds. No es necesario. Podemos definir el tipo y declarar la variable en
una sola sentencia:
struct Coleccion {
struct CompactDisc cd [MAXDISCOS];
int cantidad ;
Apuntemos ahora cómo enriquecer nuestro programa de gestión de una colección de discos
compactos almacenando, además, las canciones de cada disco. Empezaremos por definir un
nuevo registro: el que modela una canción. De cada canción nos interesa el tı́tulo, el autor y la
duración:
1 struct Cancion {
2 char titulo[LONTITULO+1];
3 char autor [LONINTERPRETE+1];
4 struct Tiempo duracion;
5 };
Hemos de modificar el registro struct CompactDisc para que almacene hasta, digamos, 20
canciones:
1 #define MAXCANCIONES 20
2
3 struct CompactDisc {
4 char titulo[LONTITULO+1];
5 char interprete[LONINTERPRETE+1];
6 struct Tiempo duracion;
7 int anyo;
8 struct Cancion cancion[MAXCANCIONES]; // Vector de canciones.
9 int canciones; // Número de canciones que realmente hay.
10 };
¿Cómo leemos ahora un disco compacto? Aquı́ tienes, convenientemente modificada, la por-
ción del programa que se encarga de ello:
1 ...
2 int main(void)
3 {
4 int segundos;
5 ...
6 switch(opcion) {
7 case Anyadir : // Añadir un CD.
8 if (mis_cds.cantidad == MAXDISCOS)
9 printf ("La base de datos está llena. Lo siento.\n");
10 else {
11 printf ("Tı́tulo: ");
12 gets(mis_cds.cd [mis_cds.cantidad ].titulo);
13 printf ("Intérprete: ");
14 gets(mis_cds.cd [mis_cds.cantidad ].interprete);
15 printf ("A~
no: ");
16 gets(linea); sscanf (linea, "%d", &mis_cds.cd [mis_cds.cantidad ].anyo);
17
18 do {
19 printf ("Número de canciones: ");
20 gets(linea); sscanf (linea, "%d", &mis_cds.cd [mis_cds.cantidad ].canciones);
21 } while (mis_cds.cd [mis_cds.cantidad ].canciones > MAXCANCIONES);
22
36 segundos = 0;
43 mis_cds.cantidad ++;
44 }
45 break;
46 ...
47 }
Observa cómo se calcula ahora la duración del compacto como suma de las duraciones de todas
sus canciones.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 146 Diseña un programa C que gestione una agenda telefónica. Cada entrada de la agenda
contiene el nombre de una persona y hasta 10 números de teléfono. El programa permitirá
añadir nuevas entradas a la agenda y nuevos teléfonos a una entrada ya existente. El menú
del programa permitirá, además, borrar entradas de la agenda, borrar números de teléfono
concretos de una entrada y efectuar búsquedas por las primeras letras del nombre. (Si, por
ejemplo, tu agenda contiene entradas para ((José Martı́nez)), ((Josefa Pérez)) y ((Jaime Primero)),
una búsqueda por ((Jos)) mostrará a las dos primeras personas y una búsqueda por ((J)) las
mostrará a todas.)
.............................................................................................
1 #define LONTITULO 80
2 #define LONINTERPRETE 40
3
4 struct Tiempo {
5 int minutos;
6 int segundos;
7 };
8
11 struct Cancion {
12 char titulo[LONTITULO+1];
13 char autor [LONINTERPRETE+1];
14 TipoTiempo duracion;
15 };
16
19
20 struct CompactDisc {
21 char titulo[LONTITULO+1];
22 char interprete[LONINTERPRETE+1];
23 TipoTiempo duracion;
24 int anyo;
25 TipoCancion cancion[MAXCANCIONES]; // Vector de canciones.
26 int canciones; // Número de canciones que realmente hay.
27 };
Hay una forma más compacta de definir un nuevo tipo a partir de un registro:
1 #define LONTITULO 80
2 #define LONINTERPRETE 40
3
4 typedef struct {
5 int minutos;
6 int segundos;
7 } TipoTiempo ;
8
9 typedef struct {
10 char titulo[LONTITULO+1];
11 char autor [LONINTERPRETE+1];
12 TipoTiempo duracion;
13 } TipoCancion ;
14
15 typedef struct {
16 char titulo[LONTITULO+1];
17 char interprete[LONINTERPRETE+1];
18 TipoTiempo duracion;
19 int anyo;
20 TipoCancion cancion[MAXCANCIONES]; // Vector de canciones.
21 int canciones; // Número de canciones que realmente hay.
22 } TipoCompactDisc ;
23
24 typedef struct {
25 TipoCompactDisc cd [MAXDISCOS];
26 int cds;
27 } TipoColeccion ;
28
29 int main(void)
30 {
31 TipoColeccion mis_cds;
32 ...
Observa que, sistemáticamente, hemos utilizado iniciales mayúsculas para los nombres de
tipos de datos (definidos con typedef y struct o sólo con struct). Es un buen convenio para
no confundir variables con tipos. Te recomendamos que hagas lo mismo o, en su defecto, que
adoptes cualquier otro criterio, pero que sea coherente.
El renombramiento de tipos no sólo sirve para eliminar la molesta palabra clave struct,
también permite diseñar programas más legibles y en los que resulta más fácil cambiar tipos
globalmente.
Imagina que en un programa nuestro representamos la edad de una persona con un valor
entre 0 y 127 (un char). Una variable edad se declararı́a ası́:
char edad ;
No es muy elegante: una edad no es un carácter, sino un número. Si definimos un ((nuevo)) tipo,
el programa es más legible:
typedef char TipoEdad;
TipoEdad edad ;
Es más, si más adelante deseamos cambiar el tipo char por int, sólo hemos de cambiar la lı́nea
que empieza por typedef , aunque hayamos definido decenas de variables del tipo TipoEdad:
typedef int TipoEdad;
TipoEdad edad ;
1 #include <stdio.h>
2
5 int main(void)
6 {
7 TipoEdad mi_edad ;
8
13 return 0;
14 }
¿Qué pasa si, posteriormente, decidimos que el tipo TipoEdad debiera ser un entero
de 32 bits? He aquı́ una versión errónea del programa:
1 #include <stdio.h>
2
5 int main(void)
6 {
7 TipoEdad mi_edad ;
8
13 return 0;
14 }
¿Y por qué es erróneo? Porque debiéramos haber modificado además las marcas de formato
de scanf y printf : en lugar de %hhu deberı́amos usar ahora %hd.
C no es un lenguaje idóneo para este tipo de modificaciones. Otros lenguajes, como C++
soportan de forma mucho más flexible la posibilidad de cambiar tipos de datos, ya que no
obligan al programador a modificar un gran número de lı́neas del programa.
Funciones
5 logbase = log10(b);
6 resultado = log10(x)/logbase;
7 return resultado;
8 }
El tipo de retorno indica de qué tipo de datos es el valor devuelto por la función como
resultado (más adelante veremos cómo definir procedimientos, es decir, funciones sin valor
de retorno). Puedes considerar esto como una limitación frente a Python: en C, cada
función devuelve valores de un único tipo. No podemos definir una función que, según
convenga, devuelva un entero, un flotante o una cadena, como hicimos en Python cuando
nos convino.
En nuestro ejemplo, la función devuelve un valor de tipo float.
5 logbase = log10(b);
6 resultado = log10(x)/logbase;
7 return resultado ;
8 }
El identificador es el nombre de la función y, para estar bien formado, debe observar las
mismas reglas que se siguen para construir nombres de variables. Eso sı́, no puedes definir
una función con un identificador que ya hayas usado para una variable (u otra función).
El identificador de nuestra función de ejemplo es logaritmo:
5 logbase = log10(b);
6 resultado = log10(x)/logbase;
7 return resultado;
8 }
Entre paréntesis aparece una lista de declaraciones de parámetros separadas por comas.
Cada declaración de parámetro indica tanto el tipo del mismo como su identificador1 .
Nuestra función tiene dos parámetros, uno de tipo float y otro de tipo int.
5 logbase = log10(b);
6 resultado = log10(x)/logbase;
7 return resultado;
8 }
El cuerpo de la función debe ir encerrado entre llaves, aunque sólo conste de una sentencia.
Puede empezar por una declaración de variables locales a la que sigue una o más sentencias
C. La sentencia return permite finalizar la ejecución de la función y devolver un valor
(que debe ser del mismo tipo que el indicado como tipo de retorno). Si no hay sentencia
return, la ejecución de la función finaliza también al acabar de ejecutar la última de las
sentencias de su cuerpo, pero es un error no devolver nada con return si se ha declarado
la función como tal, y no como procedimiento.
Nuestra función de ejemplo tiene un cuerpo muy sencillo. Hay una declaración de variables
(locales) y está formado por tres sentencias, dos de asignación y una de devolución de
valor:
1 Eso en el caso de parámetros escalares. Los parámetros de tipo vectorial se estudiarán más adelante.
5 logbase = log10(b);
6 resultado = log10(x)/logbase;
7 return resultado;
8 }
5 logbase = log10(b);
6 resultado = log10(x)/logbase;
7 return resultado;
8 }
logaritmo.c logaritmo.c
1 #include <stdio.h>
2 #include <math.h>
3
8 logbase = log10(b);
9 resultado = log10(x)/logbase;
10 return resultado;
11 }
12
17 y = logaritmo(128.0, 2) ;
18 printf ("%f\n", y);
19
20 return 0;
21 }
7.000000
Es necesario que toda función se defina en el programa antes de la primera lı́nea en que
se usa. Por esta razón, todas nuestras funciones se definen delante de la función main, que es
la función que contiene el programa principal y a la que, por tanto, no se llama desde ningún
punto del programa.2
Naturalmente, ha resultado necesario incluir la cabecera math.h en el programa, ya que
usamos la función log10. Recuerda, además, que al compilar se debe enlazar con la biblioteca
matemática, es decir, se debe usar la opción -lm de gcc.
Esta ilustración te servirá para identificar los diferentes elementos de la definición de una
función y de su invocación:
2 Nuevamente hemos de matizar una afirmación: en realidad sólo es necesario que se haya declarado el prototipo
Tipo de retorno
Identificador
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 147 Define una función que reciba un int y devuelva su cuadrado.
· 148 Define una función que reciba un float y devuelva su cuadrado.
· 149 Define una función que reciba dos float y devuelva 1 (((cierto))) si el primero es menor
que el segundo y 0 (((falso))) en caso contrario.
· 150 Define una función que calcule el volumen de una esfera a partir de su radio r. (Recuerda
que el volumen de una esfera de radio r es 4/3πr3 .)
· 151 El seno hiperbólico de x es
ex − e−x
sinh = .
2
Diseña una función C que efectúe el calculo de senos hiperbólicos. (Recuerda que ex se puede
calcular con la función exp, disponible incluyendo math.h y enlazando el programa ejecutable
con la librerı́a matemática.)
· 152 Diseña una función que devuelva ((cierto)) (el valor 1) si el año que se le suministra
como argumento es bisiesto, y ((falso)) (el valor 0) en caso contrario.
· 153 La distancia de un punto (x0 , y0 ) a una recta Ax + By + C = 0 viene dada por
Ax0 + By0 + C
d= √ .
A2 + B 2
Diseña una función que reciba los valores que definen una recta y los valores que definen un
punto y devuelva la distancia del punto a la recta.
.............................................................................................
Veamos otro ejemplo de definición de función:
1 int minimo(int a, int b, int c)
2 {
3 if (a <= b)
4 if (a <= c)
5 return a;
6 else
7 return c;
8 else
9 if (b <= c)
10 return b;
11 else
12 return c;
13 }
La función minimo devuelve un dato de tipo int y recibe tres datos, también de tipo int. No
hay problema en que aparezca más de una sentencia return en una función. El comportamiento
de return es el mismo que estudiamos en Python: tan pronto se ejecuta, finaliza la ejecución
de la función y se devuelve el valor indicado.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 154 Define una función que, dada una letra minúscula del alfabeto inglés, devuelva su
correspondiente letra mayúscula. Si el carácter recibido como dato no es una letra minúscula,
la función la devolverá inalterada.
· 155 ¿Qué error encuentras en esta función?
1 int minimo (int a, b, c)
2 {
3 if (a <= b && a <= c)
4 return a;
5 if (b <= a && b <= c)
6 return b;
7 return c;
8 }
.............................................................................................
Observa que main es una función. Su cabecera es int main(void). ¿Qué significa void?
Significa que no hay parámetros. Pero no nos adelantemos. En este mismo capı́tulo hablaremos
de funciones sin parámetros.
7 s = 0;
13 int main(void)
14 {
15 int i; // Variable local a main.
16
1 #include <stdio.h>
2
3 int main(void)
4 {
5 int i;
6
1 #include <stdio.h>
2
3 int main(void)
4 {
5 int i;
6
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 156 Diseña una función que calcule el factorial de un entero n.
· 157 Diseña una función que calcule xn , para n entero y x de tipo float. (Recuerda que si
n es negativo, xn es el resultado de multiplicar 1/x por sı́ mismo −n veces.)
· 158 El valor de la función ex puede aproximarse con el desarrollo de Taylor:
x2 x3 x4
ex ≈ 1 + x + + + + ···
2! 3! 4!
Diseña una función que aproxime el valor de ex usando n términos del desarrollo de Taylor,
siendo n un número entero positivo. (Puedes usar, si te conviene, la función de exponenciación
del último ejercicio para calcular los distintos valores de xi , aunque hay formas más eficientes
de calcular x/1!, x2 /2!, x3 /3!, . . . , ¿sabes cómo? Plantéate cómo generar un término de la forma
xi /i! a partir de un término de la forma xi−1 /(i − 1)!.)
· 159 El valor de la función coseno puede aproximarse con el desarrollo de Taylor:
x2 x4 x6
cos(x) ≈ 1 − + − + ···
2! 4! 6!
Diseña una función que aproxime el coseno de un valor x usando n términos del desarrollo de
Taylor, siendo n un número entero positivo.
· 160 Diseña una función que diga si un número es perfecto o no. Si el número es perfecto,
devolverá ((cierto)) (el valor 1) y si no, devolverá ((falso)) (el valor 0). Un número es perfecto si
es igual a la suma de todos sus divisores (excepto él mismo).
· 161 Diseña una función que diga si un número entero es o no es capicúa.
.............................................................................................
5 int doble(void)
6 {
7 i *= 2; // Referencia a la variable global i.
8 return i ; // Referencia a la variable global i.
9 }
10
11 int main(void)
12 {
13 int i ; // Variable local i.
14
18 return 0;
19 }
Fı́jate en la pérdida de legibilidad que supone el uso del identificador i en diferentes puntos
del programa: hemos de preguntarnos siempre si corresponde a la variable local o global. Te
desaconsejamos el uso generalizado de variables globales en tus programas. Como evitan usar
parámetros en funciones, llegan a resultar muy cómodas y es fácil que abuses de ellas. No es
que siempre se usen mal, pero se requiere una cierta experiencia para formarse un criterio firme
que permita decidir cuándo resulta conveniente usar una variable global y cuándo conviene
suministrar información a funciones mediante parámetros.
Como estudiante te pueden parecer un recurso cómodo para evitar suministrar información
a las funciones mediante parámetros. Ese pequeño beneficio inmediato es, creenos, un lastre a
medio y largo plazo: aumentará la probabilidad de que cometas errores al intentar acceder o
modificar una variable y las funciones que definas en un programa serán difı́cilmente reutilizables
en otros. Estás aprendiendo a programar y pretendemos evitar que adquieras ciertos vicios, ası́
que te prohibimos que las uses. . . salvo cuando convenga que lo hagas.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 162 ¿Qué muestra por pantalla este programa?
5 void fija(int a)
6 {
7 contador = a;
8 }
9
10 int decrementa(int a)
11 {
12 contador -= a;
13 return contador ;
14 }
15
21 void cuenta_atras(int a)
22 {
23 int contador ;
24 for (contador =a; contador >=0; contador --)
25 printf ("%d ", contador );
26 printf ("\n");
27 }
28
29 int main(void) {
30 int i;
31
32 contador = 10;
33 i = 1;
34 while (contador >= 0) {
35 muestra(contador );
36 cuenta_atras(contador );
37 muestra(i);
38 decrementa(i);
39 i *= 2;
40 }
41 }
.............................................................................................
Para llamar a la función dado hemos de añadir un par de paréntesis a la derecha del iden-
tificador, aunque no tenga parámetros.
Ya te habı́amos anticipado que la función main es una función sin parámetros que devuelve
un entero:
dado.c dado.c
1 #include <stdio.h>
2 #include <stdlib.h>
3
4 int dado(void)
5 {
6 return rand () % 6 + 1;
7 }
8
9 int main(void)
10 {
11 int i;
12 for (i=0; i<10; i++)
13 printf ("%d\n", dado() );
14 return 0;
15 }
int dado(void)
{
return (int) ((double) rand () / ((double) RAND_MAX + 1) * 6) + 1;
}
La constante RAND_MAX es el mayor número aleatorio que puede devolver rand . La división
hace que el número generado esté en el intervalo [0, 1[.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 163 El programa dado.c siempre genera la misma secuencia de números aleatorios. Para
evitarlo, debes proporcionar una semilla diferente con cada ejecución del programa. El valor de
la semilla se suministra como úncio argumento de la función srand . Modifı́ca dado.c para que
solicite al usuario la introducción del valor semilla.
.............................................................................................
Un uso tı́pico de las funciones sin parámetros es la lectura de datos por teclado que deben
satisfacer una serie de restricciones. Esta función, por ejemplo, lee un número entero de teclado
y se asegura de que sea par:
1 int lee_entero_par (void)
2 {
3 int numero;
4
Otro uso tı́pico es la presentación de menús de usuario con lectura de la opción seleccionada
por el usuario:
5 do {
6 printf ("1) Alta usuario\n");
7 printf ("2) Baja usuario\n");
8 printf ("3) Consulta usuario\n");
9 printf ("4) Salir\n");
10
16 return opcion;
17 }
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 164 Diseña una función que lea por teclado un entero positivo y devuelva el valor leı́do.
Si el usuario introduce un número negativo, la función advertirá del error por pantalla y leerá
nuevamente el número cuantas veces sea menester.
.............................................................................................
3.4. Procedimientos
Un procedimiento, como recordarás, es una función que no devuelve valor alguno. Los procedi-
mientos provocan efectos laterales, como imprimir un mensaje por pantalla, modificar variables
globales o modificar el valor de sus parámetros.
Los procedimientos C se declaran como funciones con tipo de retorno void. Mira este ejem-
plo:
1 #include <stdio.h>
2
3 void saludos(void)
4 {
5 printf ("Hola, mundo.\n");
6 }
En un procedimiento puedes utilizar la sentencia return, pero sin devolver valor alguno.
Cuando se ejecuta una sentencia return, finaliza inmediatamente la ejecución del procedimien-
to.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 165 Diseña un procedimiento que reciba un entero n y muestre por pantalla n asteriscos
seguidos con un salto de lı́nea al final.
· 166 Diseña un procedimiento que, dado un valor de n, dibuje con asteriscos un triángulo
rectángulo cuyos catetos midan n caracteres. Si n es 5, por ejemplo, el procedimiento mostrará
por pantalla este texto:
1 *
2 **
3 ***
4 ****
5 *****
7 do {
8 b++;
9 num /= 2;
10 } while ( num > 0);
11
12 return b;
13 }
14
15 int main(void)
16 {
17 unsigned int numero ;
18 int bitsnumero;
19
Como puedes ver, el valor de numero permanece inalterado tras la llamada a bits, aunque
en el cuerpo de la función se modifica el valor del parámetro num (que toma el valor de numero
en la llamada). Un parámetro es como una variable local, sólo que su valor inicial se obtiene
copiando el valor del argumento que suministramos. Ası́ pues, num no es numero, sino otra
variable que contiene una copia del valor de numero. Es lo que se denomina paso de parámetro
por valor.
Llegados a este punto conviene que nos detengamos a estudiar cómo se gestiona la memoria
en las llamadas a función.
bitsnumero
main
numero
bitsnumero
main
numero 128
Cuando se produce la llamada a la función bits, se crea una nueva trama de activación:
b
bits
num
llamada desde lı́nea 21
bitsnumero
main
numero 128
El parámetro num recibe una copia del contenido de numero y se inicializa la variable local b
con el valor 0:
b 0
bits
num 128
llamada desde lı́nea 21
bitsnumero
main
numero 128
Tras ejecutar el bucle de bits, la variable b vale 8. Observa que aunque num ha modificado su
valor y éste provenı́a originalmente de numero, el valor de numero no se altera:
b 8
bits
num 0
llamada desde lı́nea 21
bitsnumero
main
numero 128
La trama de activación de bits desaparece ahora, pero dejando constancia del valor devuelto
por la función:
return 8
bitsnumero
main
numero 128
bitsnumero 8
main
numero 128
Como ves, las variables locales sólo ((viven)) durante la ejecución de cada función. C obtiene
una copia del valor de cada parámetro y la deja en la pila. Cuando modificamos el valor de un
parámetro en el cuerpo de la función, estamos modificando el valor del argumento, no el de la
variable original.
Este otro programa declara numero como una variable global y trabaja directamente con
dicha variable:
numbits2.c numbits2.c
1 #include <stdio.h>
2
9 do {
10 b++;
11 numero /= 2;
12 } while (numero > 0);
13
14 return b;
15 }
16
17 int main(void)
18 {
19 int bitsnumero;
20
Las variables globales residen en una zona especial de la memoria y son accesibles desde cual-
quier función. Representaremos dicha zona como un área enmarcada con una lı́nea discontı́nua.
Cuando se inicia la ejecución del programa, ésta es la situación:
variables globales
variables globales
bits
b
llamada desde lı́nea 22
El cálculo de bits modifica el valor de numero. Tras la primera iteración del bucle while, ésta
es la situación:
variables globales
bits
b 1
llamada desde lı́nea 22
return 8
Bueno. Ahora sabes qué pasa con las variables globales y cómo acceder a ellas desde las
funciones. Pero repetimos lo que te dijimos al aprender Python: pocas veces está justificado
acceder a variables globales, especialmente cuando estás aprendiendo. Evı́talas.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 168 Estudia este programa y muestra gráficamente el contenido de la memoria cuando se
van a ejecutar por primera vez las lı́neas 24, 14 y 5.
suma cuadrados.c suma cuadrados.c
1 #include <stdio.h>
2
3 int cuadrado(int i)
4 {
5 return i * i;
6 }
7
12 s = 0;
13 for (i=a; i<=b; i++)
14 s += cuadrado(i);
15 return s;
16 }
17
18 int main(void)
19 {
20 int i, j;
21
22 i = 10;
23 j = 20;
24 printf ("%d\n", sumatorio(i, j));
25 return 0;
26 }
· 169 Este programa muestra por pantalla los 10 primeros números primos. La función si-
guiente genera cada vez un número primo distinto. Gracias a la variable global ultimoprimo la
función ((recuerda)) cuál fue el último número primo generado. Haz una traza paso a paso del
programa (hasta que haya generado los 4 primeros primos). Muestra el estado de la pila y el
de la zona de variables globales en los instantes en que se llama a la función siguienteprimo y
cuando ésta devuelve su resultado
diez primos.c diez primos.c
1 #include <stdio.h>
2
3 int ultimoprimo = 0;
4
5 int siguienteprimo(void)
6 {
7 int esprimo , i ;
8
9 do {
10 ultimoprimo++;
11 esprimo = 1;
12 for (i=2; i<ultimoprimo/2; i++)
13 if (ultimoprimo % i == 0) {
14 esprimo = 0;
15 break;
16 }
17 } while (!esprimo);
18 return ultimoprimo;
19 }
20
21 int main(void)
22 {
23 int i ;
24
3 #define N 10
4
14 /* Inicialización */
15 criba[0] = 0;
16 for (i=1; i<n; i++)
17 criba[i] = 1;
18
19 /* Criba de Eratóstenes */
20 for (i=2; i<n; i++)
21 if (criba[i])
22 for (j=2; i*j<n; j++)
23 criba[i*j] = 0;
24
25 /* Conteo de primos */
26 numprimos = 0;
27 for (i=0; i<n; i++)
28 if (criba[i])
29 numprimos++;
30
31 return numprimos;
32 }
33
34
35 int main(void)
36 {
37 int hasta, cantidad ;
38
Cuando el programa inicia su ejecución, se crea una trama de activación en la que se albergan
las variables hasta y cantidad . Supongamos que cuando se solicita el valor de hasta el usuario
introduce el valor 6. He aquı́ el aspecto de la memoria:
hasta 6
main
cantidad
Se efectúa entonces (lı́nea 40) la llamada a cuenta_primos, con lo que se crea una nueva tra-
ma de activación. En ella se reserva memoria para todas las variables locales de cuenta_primos:
n
0 1 2 3 4 5 6 7 8 9
criba
cuenta primos
j
i
numprimos
llamada desde lı́nea 40
hasta 6
main
cantidad
Observa que el vector criba ocupa memoria en la propia trama de activación. Completa tú
mismo el resto de acciones ejecutadas por el programa ayudándote de una traza de la pila de
llamadas a función con gráficos como los mostrados.
8 /* Inicialización */
9 criba[0] = 0;
10 for (i=1; i<n; i++)
11 criba[i] = 1;
12
13 /* Criba de Eratóstenes */
14 for (i=2; i<n; i++)
15 if (criba[i])
16 for (j=2; i*j<n; j++)
17 criba[i*j] = 0;
18
19 /* Conteo de primos */
20 numprimos = 0;
21 for (i=0; i<n; i++)
22 if (criba[i])
23 numprimos++;
24
25 return numprimos;
26 }
27
28
29 int main(void)
30 {
31 int hasta, cantidad ;
32
Fı́jate en cómo hemos definido el vector criba: la talla no es un valor constante, sino n, un
parámetro cuyo valor es desconocido hasta el momento en que se ejecute la función. Esta es
una caracterı́stica de C99 y supone una mejora interesante del lenguaje.
3 #define TALLA 3
13 int main(void)
14 {
15 int i, v[TALLA];
16
17
¡El contenido de v se ha modificado! Ocurre lo mismo que ocurrı́a en Python: los vectores sı́
modifican su contenido cuando se altera el contenido del respectivo parámetro en las llamadas
a función.
Cuando se pasa un parámetro vectorial a una función no se efectúa una copia de su contenido
en la pila: sólo se copia la referencia a la posición de memoria en la que empieza el vector.
¿Por qué? Por eficiencia: no es infrecuente que los programas manejen vectores de tamaño
considerable; copiarlos cada vez en la pila supondrı́a invertir una cantidad de tiempo que, para
vectores de tamaño medio o grande, podrı́a ralentizar drásticamente la ejecución del programa.
La aproximación adoptada por C hace que sólo sea necesario copiar en la pila 4 bytes, que es
lo que ocupa una dirección de memoria. Y no importa cuán grande o pequeño sea un vector: la
dirección de su primer valor siempre ocupa 4 bytes.
Veamos gráficamente, pues, qué ocurre en diferentes instantes de la ejecución del programa.
Justo antes de ejecutar la lı́nea 23 tenemos esta disposición de elementos en memoria:
0 1 2
v 0 1 2
main
i 3
a
incrementa
i 0
llamada desde lı́nea 23
0 1 2
v 0 1 2
main
i 3
¿Ves? El parámetro a apunta a v. Los cambios sobre elementos del vector a que tienen lugar al
ejecutar la lı́nea 10 tienen efecto sobre los correspondientes elementos de v, ası́ que v refleja los
cambios que experimenta a. Tras ejecutar el bucle de incrementa, tenemos esta situación:
a
incrementa
i 3
llamada desde lı́nea 23
0 1 2
v 1 2 3
main
i 3
0 1 2
v 1 2 3
main
i 3
¿Y qué ocurre cuando el vector es una variable global? Pues básicamente lo mismo: las refe-
rencias no tienen por qué ser direcciones de memoria de la pila. Este programa es básicamente
idéntico al anterior, sólo que v es ahora una variable global:
pasa vector 1.c pasa vector.c
1 #include <stdio.h>
2
3 #define TALLA 3
4
5 int v[TALLA];
6
15 int main(void)
16 {
17 int i;
18
21 v[i] = i;
22 printf ("%d: %d\n", i, v[i]);
23 }
24 incrementa(v) ;
25 printf ("Después de llamar a incrementa:\n");
26 for (i=0; i<TALLA; i++)
27 printf ("%d: %d\n", i, v[i]);
28 return 0;
29 }
Analicemos qué ocurre en diferentes instantes de la ejecución del programa. Justo antes de
ejecutar la lı́nea 24, existen las variables locales a main y las variables globales:
variables globales
0 1 2
main i 3 v 0 1 2
a
variables globales
incrementa i 0
llamada desde lı́nea 24
0 1 2
main i 3 v 0 1 2
a
variables globales
incrementa i 3
llamada desde lı́nea 24
0 1 2
main i 3 v 1 2 3
0 1 2
main i 3 v 1 2 3
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 170 Diseña un programa C que manipule polinomios de grado menor o igual que 10. Un
polinomio se representará con un vector de float de tamaño 11. Si p es un vector que representa
un polinomio, p[i] es el coeficiente del término de grado i. Diseña un procedimiento suma con
el siguiente perfil:
void suma(float p[], float q[], float r[])
El procedimiento modificará r para que contenga el resultado de sumar los polinomios p y q.
· 171 Diseña una función que, dada una cadena y un carácter, diga cuántas veces aparece el
carácter en la cadena.
.............................................................................................
Hemos visto cómo pasar vectores a funciones. Has de ser consciente de que no hay forma de
saber cuántos elementos tiene el vector dentro de una función: fı́jate en que no se indica cuántos
elementos tiene un parámetro vectorial. Si deseas utilizar el valor de la talla de un vector tienes
dos posibilidades:
1. saberlo de antemano,
2. o proporcionarlo como parámetro adicional.
Estudiemos la primera alternativa. Fı́jate en este fragmento de programa:
pasa vector talla.c pasa vector talla.c
1 #include <stdio.h>
2
3 #define TALLA1 20
4 #define TALLA2 10
5
23 int main(void)
24 {
25 int x[ TALLA1 ];
26 int y[TALLA2];
27
28 inicializa(x);
!
29 inicializa(y); // Ojo!
30
31 imprime(x);
!
32 imprime(y); // Ojo!
33
34 return 0;
35 }
Siguiendo esta aproximación, la función inicializa sólo se puede utilizar con vectores de int de
talla TALLA1, como x. No puedes llamar a inicializa con y: si lo haces (¡y C te deja hacerlo!)
cometerás un error de acceso a memoria que no te está reservada, pues el bucle recorre TALLA1
componentes, aunque y sólo tenga TALLA2. Ese error puede abortar la ejecución del programa
o, peor aún, no haciéndolo pero alterando la memoria de algún modo indefinido.
Este es el resultado obtenido en un ordenador concreto:
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
3 #define TALLA1 20
4 #define TALLA2 10
5
23
24 int main(void)
25 {
26 int x[TALLA1];
27 int y[TALLA2];
28
29 inicializa(x, TALLA1 );
30 inicializa(y, TALLA2 );
31
32 imprime(x, TALLA1 );
33 imprime(y, TALLA2 );
34
35 return 0;
36 }
Ahora puedes llamar a la función inicializa con inicializa(x, TALLA1) o inicializa(y, TALLA2).
Lo mismo ocurre con imprime. El parámetro talla toma el valor apropiado en cada caso porque
tú se lo estás pasando explı́citamente.
Éste es el resultado de ejecutar el programa ahora:
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0
Correcto.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 172 Diseña un procedimiento ordena que ordene un vector de enteros. El procedimiento
recibirá como parámetros un vector de enteros y un entero que indique el tamaño del vector.
· 173 Diseña una función que devuelva el máximo de un vector de enteros. El tamaño del
vector se suministrará como parámetro adicional.
· 174 Diseña una función que diga si un vector de enteros es o no es palı́ndromo (devolviendo
1 o 0, respectivamente). El tamaño del vector se suministrará como parámetro adicional.
· 175 Diseña una función que reciba dos vectores de enteros de idéntica talla y diga si son
iguales o no. El tamaño de los dos vectores se suministrará como parámetro adicional.
· 176 Diseña un procedimiento que reciba un vector de enteros y muestre todos sus com-
ponentes en pantalla. Cada componente se representará separado del siguiente con una coma.
El último elemento irá seguido de un salto de lı́nea. La talla del vector se indicará con un
parámetro adicional.
· 177 Diseña un procedimiento que reciba un vector de float y muestre todos sus componentes
en pantalla. Cada componente se representará separado del siguiente con una coma. Cada 6
componentes aparecerá un salto de lı́nea. La talla del vector se indicará con un parámetro
adicional.
.............................................................................................
8 int main(void)
9 {
10 int b;
11
12 b = 1;
13 printf ("Al principio b vale %d\n", b);
14 incrementa( &b );
15 printf ("Y al final vale %d\n", b);
16 return 0;
17 }
incrementa a
llamada desde lı́nea 14
main b 1
*a += 1;
variable.) O sea, C interpreta *a como accede a la variable apuntada por a, que es b, ası́ que
*a += 1 equivale a b += 1 e incrementa el contenido de la variable b.
¿Qué pasarı́a si en lugar de *a += 1 hubiésemos escrito a += 1? Se hubiera incrementado la
dirección de memoria a la que apunta el puntero, nada más.
¿Y si hubiésemos escrito a++? Lo mismo: hubiésemos incrementado el valor de la dirección
almacenada en a. ¿Y *a++?, ¿funcionarı́a? A primera vista dirı́amos que sı́, pero no funciona
como esperamos. El operador ++ tiene mayor nivel de precedencia que el operador unario *, ası́
que *a++ (post)incrementa la dirección a y accede a su contenido, por ese órden. Nuevamente
habrı́amos incrementado el valor de la dirección de memoria, y no su contenido. Si quieres usar
operadores de incremento/decremento, tendrás que utilizar paréntesis para que los operadores
se apliquen en el orden deseado: (*a)++.
Naturalmente, no sólo puedes acceder ası́ a variables locales, también las variables globales
son accesibles mediante punteros:
referencia global.c referencia global.c
1 #include <stdio.h>
2
10 int main(void)
11 {
12 b = 1;
13 printf ("Al principio b vale %d\n", b);
14 incrementa( &b );
15 printf ("Y al final vale %d\n", b);
16 return 0;
17 }
variables globales
incrementa a
llamada desde lı́nea 14
main b 1
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 178 Diseña un procedimiento que modifique el valor del parámetro de tipo float para
que valga la inversa de su valor cuando éste sea distinto de cero. Si el número es cero, el
procedimiento dejará intacto el valor del parámetro.
Si a vale 2.0, por ejemplo, inversa(&a) hará que a valga 0.5.
· 179 Diseña un procedimiento que intercambie el valor de dos números enteros.
Si a y b valen 1 y 2, respectivamente, la llamada intercambia(&a, &b) hará que a pase a
valer 2 y b pase a valer 1.
· 180 Diseña un procedimiento que intercambie el valor de dos números float.
dualidad.c dualidad.c
1 #include <stdio.h>
2
3 #define TALLA 10
4
11 int main(void)
12 {
13 int x[TALLA], i, y = 10;
14
23 return 0;
24 }
· 181 Diseña un procedimiento que asigne a todos los elementos de un vector de enteros un
valor determinado. El procedimiento recibirá tres datos: el vector, su número de elementos y el
valor que que asignamos a todos los elementos del vector.
· 182 Diseña un procedimiento que intercambie el contenido completo de dos vectores de
enteros de igual talla. La talla se debe suministrar como parámetro.
program referencia;
var b : integer;
C++ es una extensión de C que permite el paso de parámetros por referencia. Usa para ello
el carácter & en la declaración del parámetro:
1 #include <stdio.h>
2
8 int main(void)
9 {
10 int b;
11
12 b = 1;
13 printf ("Al principio b vale %d\n", b);
14 incrementa( b );
15 printf ("Y al final vale %d\n", b);
16 return 0;
17 }
(Aunque no venga a cuento, observa lo diferente que es C de Pascal (y aun ası́, lo semejante
que es) y cómo el programa C++ presenta un aspecto muy semejante a uno equivalente
escrito en C.)
· 183 Diseña un procedimiento que asigne a un entero la suma de los elementos de un vector
de enteros. Tanto el entero (su dirección) como el vector se suministrarán como parámetros.
.............................................................................................
Un uso habitual del paso de parámetros por referencia es la devolución de más de un valor
como resultado de la ejecución de una función. Veámoslo con un ejemplo. Diseñemos una función
que, dados un ángulo α (en radianes) y un radio r, calcule el valor de x = r cos(α) e y = r sin(α):
r y
α
x
No podemos diseñar una función que devuelva los dos valores. Hemos de diseñar un procedi-
miento que devuelva los valores resultantes como parámetros pasados por referencia:
paso por referencia.c
1 #include <stdio.h>
2 #include <math.h>
3
8 }
9
10 int main(void)
11 {
12 float r, angulo, horizontal , vertical ;
13
19 return 0;
20 }
¿Ves? Las variables horizontal y vertical no se inicializan en main: reciben valores como resul-
tado de la llamada a calcula_xy.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 184 Diseña una función que calcule la inversa de calcula_xy, es decir, que obtenga el valor
del radio y del ángulo a partir de x e y.
· 185 Diseña una función que reciba dos números enteros a y b y devuelva, simultáneamente,
el menor y el mayor de ambos. La función tendrá esta cabecera:
1 void minimax (int a, int b, int * min, int * max )
· 186 Diseña una función que reciba un vector de enteros, su talla y un valor de tipo entero al
que denominamos buscado. La función devolverá (mediante return) el valor 1 si buscado tiene el
mismo valor que algún elemento del vector y 0 en caso contrario. La función devolverá, además,
la distancia entre buscado y el elemento más próximo a él.
La cabecera de la función ha de ser similar a ésta:
1 int busca(int vector [], int talla, int buscado, int * distancia)
Te ponemos un par de ejemplos para que veas qué debe hacer la función.
1 #include <stdio.h>
2
3 #define TALLA 6
8 int main(void)
9 {
10 int v[TALLA], distancia, encontrado, buscado, i;
11
35 return 0;
36 }
· 187 Modifica la función del ejercicio anterior para que, además de la distancia al elemento
más próximo, devuelva el valor del elemento más próximo.
· 188 Modifica la función del ejercicio anterior para que, además de la distancia al elemento
más próximo y el elemento más próximo, devuelva el valor de su ı́ndice.
.............................................................................................
1 #include <stdio.h>
2 #include <math.h>
3
4 struct Punto {
5 float x, y, z;
6 };
7
13 int main(void)
14 {
15 struct Punto pto;
16
17 pto.x = 1;
18 pto.y = 1;
19 pto.z = 1;
20
Al pasar un registro a la función, C copia en la pila cada uno de los valores de sus campos. Ten
en cuenta que una variable de tipo struct Punto ocupa 24 bytes (contiene 3 valores de tipo
float). Variables de otros tipos registro que definas pueden ocupar cientos o incluso miles de
bytes, ası́ que ve con cuidado: llamar a una función pasando registros por valor puede resultar
ineficiente. Por cierto, no es tan extraño que un registro ocupe cientos de bytes: uno o más de
sus campos podrı́a ser un vector. También en ese caso se estarı́a copiando su contenido ı́ntegro
en la pila.
Eso sı́, como estás pasando una copia, las modificaciones del valor de un campo en el cuerpo
de la función no tendrán efectos perceptibles fuera de la función.
Como te hemos anticipado, también puedes pasar registros por referencia. En tal caso sólo
se estará copiando en la pila la dirección de memoria en la que empieza el registro (y eso son
4 bytes), mida lo que mida éste. Se trata, pues, de un paso de parámetros más eficiente. Eso
sı́, has de tener en cuenta que los cambios que efectúes a cualquier campo del parámetro se
reflejarán en el campo correspondiente de la variable que suministraste como argumento.
Esta función, por ejemplo, define dos parámetros: uno que se pasa por referencia y otro
que se pasa por valor. La función traslada un punto p en el espacio (modificando los campos
del punto original) de acuerdo con el vector de desplazamiento que se indica con otro punto
(traslacion):
1 void traslada( struct Punto * p , struct Punto traslacion)
2 {
3 (*p).x += traslacion.x;
4 (*p).y += traslacion.y;
5 (*p).z += traslacion.z;
6 }
Observa cómo hemos accedido a los campos de p. Ahora p es una dirección de memoria (es de
tipo struct Punto *), y *p es la variable apuntada por p (y por tanto, es de tipo struct Punto).
El campo x es accedido con (*p).x: primero se accede al contenido de la dirección de memoria
apuntada por p, y luego al campo x del registro *p, de ahı́ que usemos paréntesis.
Es tan frecuente la notación (*p).x que existe una forma compacta equivalente:
1 void traslada(struct Punto * p, struct Punto traslacion)
2 {
3 p->x += traslacion.x;
4 p->y += traslacion.y;
5 p->z += traslacion.z;
6 }
Recuerda, pues, que dentro de una función se accede a los campos de forma distinta según
se pase un valor por copia o por referencia:
2. con el operador ((flecha)), como en p->x, si la variable se ha pasado por referencia (equi-
valentemente, puedes usar la notación (*p).x).
Acabemos este apartado mostrando una rutina que pide al usuario que introduzca las coor-
denadas de un punto:
1 void lee_punto(struct Punto * p)
2 {
3 printf ("x: "); scanf ("%f", &p->x);
4 printf ("y: "); scanf ("%f", &p->y);
5 printf ("z: "); scanf ("%f", &p->z);
6 }
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 189 Este ejercicio y los siguientes de este bloque tienen por objeto construir una serie de
funciones que permitan efectuar transformaciones afines sobre puntos en el plano. Los puntos
serán variables de tipo struct Punto, que definimos ası́:
1 struct Punto {
2 float x, y;
3 };
Diseña un procedimiento muestra_punto que muestre por pantalla un punto. Un punto p tal que
p.x vale 2.0 y p.y vale 0.2 se mostrará en pantalla ası́: (2.000000, 0.200000). El procedimiento
muestra_punto recibirá un punto por valor.
Diseña a continuación un procedimiento que permita leer por teclado un punto. El procedi-
miento recibirá por referencia el punto en el que se almacenarán los valores leı́dos.
· 190 La operación de traslación permite desplazar un punto de coordenadas (x, y) a (x+a, y+
b), siendo el desplazamiento (a, b) un vector (que representamos con otro punto). Implementa
una función que reciba dos parámetros de tipo punto y modifique el primero de modo que se
traslade lo que indique el vector.
· 191 La operación de escalado transforma un punto (x, y) en otro (ax, ay), donde a es un
factor de escala (real). Implementa una función que escale un punto de acuerdo con el factor
de escala a que se suministre como parámetro (un float).
· 192 Si rotamos un punto (x, y) una cantidad de θ radianes alrededor del origen, obtenemos
el punto
(x cos θ − y sin θ, x sin θ + y cos θ).
Define una función que rote un punto la cantidad de grados que se especifique.
· 193 La rotación de un punto (x, y) una cantidad de θ radianes alrededor de un punto (a, b)
se puede efectuar con una traslación con el vector (−a, −b), una rotación de θ radianes con
respecto al origen y una nueva traslación con el vector (a, b). Diseña una función que permita
trasladar un punto un número dado de grados alrededor de otro punto.
· 194 Diseña una función que diga si dos puntos son iguales.
· 195 Hemos definido un tipo registro para representar complejos ası́:
1 struct Complejo {
2 float real ;
3 float imag;
4 };
√
el módulo de un complejo (|a + bi| = a2 + b2 );
el opuesto de un complejo (−(a + bi) = −a − bi);
el conjugado de un complejo (a + bi = a − bi);
la suma de dos complejos ((a + bi) + (c + di) = (a + c) + (b + d)i);
la diferencia de dos complejos ((a + bi) − (c + di) = (a − c) + (b − d)i);
el producto de dos complejos ((a + bi) · (c + di) = (ac − bd) + (ad + bc)i);
la división de dos complejos ( a+bi
c+di =
ac+bd
c2 +d2 + bc−ad
c2 +d2 i).
· 196 Define un tipo registro y una serie de funciones para representar y manipular fechas.
Una fecha consta de un dı́a, un mes y un año. Debes implementar funciones que permitan:
mostrar una fecha por pantalla con formato dd /mm/aaaa (por ejemplo, el 7 de junio de
2001 se muestra ası́: 07/06/2001);
mostrar una fecha por pantalla como texto (por ejemplo, el 7 de junio de 2001 se muestra
ası́: 7 de junio de 2001);
leer una fecha por teclado;
averiguar si una fecha cae en año bisiesto;
averiguar si una fecha es anterior, igual o posterior a otra, devolviendo los valores −1, 0
o 1 respectivamente,
comprobar si una fecha existe (por ejemplo, el 29 de febrero de 2002 no existe):
calcular la diferencia de dı́as entre dos fechas.
.............................................................................................
3 #define TALLA 3
4
9 m = a[0][0];
10 for (i=0; i<TALLA; i++)
11 for (j=0; j<TALLA; j++)
12 if (a[i][j] > m)
13 m = a[i][j];
14
15 return m;
16 }
17
18 int main(void)
19 {
20 int matriz [TALLA][TALLA];
21 int i, j;
22
El compilador no acepta ese programa. ¿Por qué? Fı́jate en la declaración del parámetro. ¿Qué
hay de malo? C no puede resolver los accesos de la forma a[i][j]. Si recuerdas, a[i][j] significa
((accede a la celda cuya dirección se obtiene sumando a la dirección a el valor i * COLUMNAS +
j)), donde COLUMNAS es el número de columnas de la matriz a (en nuestro caso, serı́a TALLA).
Pero, ¿cómo sabe la función cuántas columnas tiene a? ¡No hay forma de saberlo viendo una
definición del estilo int a[][]!
La versión correcta del programa debe indicar explı́citamente cuántas columnas tiene la
matriz. Hela aquı́:
pasa matriz.c pasa matriz.c
1 #include <stdio.h>
2
3 #define TALLA 3
4
9 m = a[0][0];
10 for (i=0; i<TALLA; i++)
11 for (j=0; j<TALLA; j++)
12 if (a[i][j] > m)
13 m = a[i][j];
14
15 return m;
16 }
17
18 int main(void)
19 {
20 int matriz [TALLA][TALLA];
21 int i, j;
22
Diseña una función que detecte si algún jugador consiguió hacer tres en raya.
Diseña una función que solicite al usuario la jugada de los cı́rculos y modifique el tablero
adecuadamente. La función debe detectar si la jugada es válida o no.
Diseña una función que, dado un tablero, realice la jugada que corresponde a las cruces.
En una primera versión, haz que el ordenador ponga la cruz en la primera casilla libre.
Después, modifica la función para que el ordenador realice la jugada más inteligente.
Cuando hayas diseñado todas las funciones, monta un programa que las use y permita jugar al
tres en raya contra el computador.
· 198 El juego de la vida se juega sobre una matriz cuyas celdas pueden estar vivas o muertas.
La matriz se modifica a partir de su estado siguiendo unas sencilla reglas que tienen en cuenta
los, como mucho, 8 vecinos de cada casilla:
Si una celda viva está rodeada por 0 o 1 celdas vivas, muere de soledad.
Si una celda viva está rodeada por 4 celdas vivas, muere por superpoblación.
Si una celda viva está rodeada por 2 o 3 celdas vivas, sigue viva.
Una celda muerta sólo resucita si está rodeada por 3 celdas vivas.
Diseña una función que reciba una matriz de 10 × 10 celdas en la que el valor 0 representa
((celda muerta)) y el valor 1 representa ((celda viva)). La función modificará la matriz de acuerdo
con las reglas del juego de la vida. (Avisos: Necesitarás una matriz auxiliar. Las celdas de los
bordes no tienen 8 vecinos, sino 3 o 5.)
A continuación, monta un programa que permita al usuario introducir una disposición inicial
de celdas y ejecutar el juego de la vida durante n ciclos, siendo n un valor introducido por el
usuario.
Aquı́ tienes un ejemplo de ((partida)) de 3 ciclos con una configuración inicial curiosa:
Configuración inicial:
__________
______xxx_
__________
__________
___xxx____
__xxx_____
__________
__________
__________
__________
Ciclos: 3
_______x__
_______x__
_______x__
____x_____
__x__x____
__x__x____
___x______
__________
__________
__________
__________
______xxx_
__________
__________
___xxx____
__xxx_____
__________
__________
__________
__________
_______x__
_______x__
_______x__
____x_____
__x__x____
__x__x____
___x______
__________
__________
__________
· 199 Implementa el juego del buscaminas. El juego del buscaminas se juega en un tablero
de dimensiones dadas. Cada casilla del tablero puede contener una bomba o estar vacı́a. Las
bombas se ubican aleatoriamente. El usuario debe descubrir todas las casillas que no contienen
bomba. Con cada jugada, el usuario descubre una casilla (a partir de sus coordenadas, un par
de letras). Si la casilla contiene una bomba, la partida finaliza con la derrota del usuario. Si la
casilla está libre, el usuario es informado de cuántas bombas hay en las (como mucho) 8 casillas
vecinas.
Este tablero representa, en un terminal, el estado actual de una partida sobre un tablero de
8 × 8:
abcdefgh
a 00001___
b 00112___
c 222_____
d ________
e ____3___
f ________
g 1_111111
h __100000
Las casillas con un punto no han sido descubiertas aún. Las casillas con un número han sido
descubiertas y sus casillas vecinas contienen tantas bombas como se indica en el número. Por
ejemplo, la casilla de coordenadas (’e’, ’e’) tiene 3 bombas en la vecindad y la casilla de
coordenadas (’b’, ’a’), ninguna.
Implementa un programa que permita seleccionar el nivel de dificultad y, una vez escogido,
genere un tablero y permita jugar con él al jugador.
Los niveles de dificultad son:
Debes diseñar funciones para desempeñar cada una de las acciones básicas de una partida:
dado un tablero y las coordenadas de una casilla, indicar si contiene bomba o no,
dado un tablero y las coordenadas de una casilla, devolver el número de bombas vecinas,
dado un tablero y las coordenadas de una casilla, modificar el tablero para indicar que la
casilla en cuestión ya ha sido descubierta,
etc.
.............................................................................................
9 q.x = a * p.x;
10 q.y = a * p.y;
11
12 return q;
13 }
5 ++++++++++++++++++++
6 ++++++++++++++++++++
7 ++++++++++++++++++++
8 ++++++++++++++++++++
Hay 5 náufragos.
Dispones de 20 sondas.
Coordenadas:
El tablero se muestra como una serie de casillas. Arriba tienes letras para identificar las
columnas y a la izquierda números para las filas. El ordenador nos informa de que aún quedan
5 náufragos por rescatar y que disponemos de 20 sondas. Se ha detenido mostrando el mensaje
((Coordenadas:)): está esperando a que digamos en qué coordenadas lanzamos una sonda. El
ordenador acepta una cadena que contenga un dı́gito y una letra (en cualquier orden) y la letra
puede ser minúscula o mayúscula. Lancemos nuestra primera sonda: escribamos 5b y pulsemos
la tecla de retorno de carro. He aquı́ el resultado:
Coordenadas: 5b
ABCDEFGHIJKLMNOPQRST
0 +.++++++++++++++++++
1 +.++++++++++++++++++
2 +.++++++++++++++++++
3 +.++++++++++++++++++
4 +.++++++++++++++++++
5 .0..................
6 +.++++++++++++++++++
7 +.++++++++++++++++++
8 +.++++++++++++++++++
Hay 5 náufragos.
Dispones de 19 sondas.
Coordenadas:
7 +.++++++.+++++++++++
8 +.++++++.+++++++++++
Hay 5 náufragos.
Dispones de 17 sondas.
Coordenadas:
Dos náufragos detectados. Parece probable que uno de ellos esté en la columna I. Lancemos
otra sonda en esa columna. Probemos con 2I:
Coordenadas: 2I
ABCDEFGHIJKLMNOPQRST
0 ........2...........
1 +.++++++.+++++++++++
2 ........X...........
3 ........1...........
4 +.++++++.+++++++++++
5 .0..................
6 +.++++++.+++++++++++
7 +.++++++.+++++++++++
8 +.++++++.+++++++++++
Hay 4 náufragos.
Dispones de 16 sondas.
Coordenadas:
¡Bravo! Hemos encontrado a uno de los náufragos. En el tablero se muestra con una X. Ya
sólo quedan 4.
Bueno. Con esta partida inacabada puedes hacerte una idea detallada del juego. Diseñemos
el programa.
Empezamos por definir las estructuras de datos. La primera de ellas, el tablero de juego, que
es una simple matriz de 9 × 20 casillas. Nos vendrá bien disponer de constantes que almacenen
el número de filas y columnas para usarlas en la definición de la matriz:
1 #include <stdio.h>
2
3 #define FILAS 9
4 #define COLUMNAS 20
5
6 int main(void)
7 {
8 char espacio[FILAS][COLUMNAS];
9
10 return 0;
11 }
La matriz espacio es una matriz de caracteres. Hemos de inicializarla con caracteres ’+’, que
indican que no se han explorado sus casillas. En lugar de inicializarla en main, vamos a diseñar
una función especial para ello. ¿Por qué? Para mantener main razonablemente pequeño y
mejorar ası́ la legibilidad. A estas alturas no debe asustarnos definir funciones para las diferentes
tareas.
1 #include <stdio.h>
2
3 #define FILAS 9
4 #define COLUMNAS 20
5
16 }
17
18 int main(void)
19 {
20 char espacio[FILAS][COLUMNAS];
21
22 inicializa_tablero(espacio);
23
24 return 0;
25 }
1 #include <stdio.h>
2
3 #define FILAS 9
4 #define COLUMNAS 20
5
8 ...
9
28 int main(void)
29 {
30 char espacio[FILAS][COLUMNAS];
31
32 inicializa_tablero(espacio);
33 muestra_tablero(espacio);
34
35 return 0;
36 }
cualquier matriz (siempre que su dimensión se ajuste a lo esperado), aunque nosotros sólo usaremos la matriz
espacio como argumento. Si hubiésemos usado el mismo nombre, es probable que hubiésemos alimentado la
confusión entre parámetros y argumentos que experimentáis algunos.
1 ...
2 #define TALLACAD 80
3 ...
4 int main(void)
5 {
6 ...
7 char coordenadas[TALLACAD+1];
8
9 ...
10
Como ves, las coordenadas se leerán en una cadena. Nos convendrá disponer, pues, de una
función que ((traduzca)) esa cadena a un par de números y otra que haga lo contrario:
1 void de_fila_y_columna_a_numero_y_letra(int fila, int columna, char * coordenadas)
2 /* Convierte una fila y columna descritas numéricamente en una fila y columna descritas
3 * como una cadena con un dı́gito y una letra.
4 */
6 {
7 coordenadas[0] = ’0’ + fila;
8 coordenadas[1] = ’A’ + columna;
9 coordenadas[2] = ’\0’;
10 }
11
#define MAX_NAUFRAGOS 5
struct Naufrago {
int fila, columna; // Coordenadas
?
int encontrado; // Ha sido encontrado ya?
};
struct GrupoNaufragos {
struct Naufrago naufrago[MAX_NAUFRAGOS];
int cantidad ;
};
...
El tipo registro struct Naufrago mantiene la posición de un náufrago y permite saber si sigue
perdido o si, por el contrario, ya ha sido encontrado. El tipo registro struct GrupoNaufragos
mantiene un vector de náufragos de talla MAX_NAUFRAGOS. Aunque el juego indica que hemos de
trabajar con 5 náufragos, usaremos un campo adicional con la cantidad de náufragos realmente
almacenados en el vector. De ese modo resultará sencillo modificar el juego (como te propone-
mos en los ejercicios al final de esta sección) para que se juegue con un número de náufragos
seleccionado por el usuario.
Guardaremos los náufragos en una variable de tipo struct GrupoNaufragos:
1 ...
2
3 int main(void)
4 {
5 char espacio[FILAS][COLUMNAS];
6 struct GrupoNaufragos losNaufragos;
7
8 inicializa_tablero(espacio);
9 muestra_tablero(espacio);
10
11 return 0;
12 }
El programa deberı́a empezar realmente por inicializar el registro losNaufragos ubicando a cada
náufrago en una posición aletoria del tablero. Esta función (errónea) se encarga de ello:
...
#include <stdlib.h>
...
void pon_naufragos(struct GrupoNaufragos * grupoNaufragos, int cantidad )
/* Situa aleatoriamente cantidad náufragos en la estructura grupoNaufragos. */
/* PERO LO HACE MAL. */
{
int fila, columna;
grupoNaufragos->cantidad = 0;
¿Por qué está mal? Primero hemos de entenderla bien. Analicémosla paso a paso. Empecemos
por la cabecera: la función tiene dos parámetros, uno que es una referencia (un puntero) a un
registro de tipo struct GrupoNaufragos y un entero que nos indica cuántos náufragos hemos
de poner al azar. La rutina empieza inicializando a cero la cantidad de náufragos ya dispuestos
mediante una lı́nea como ésta:
grupoNaufragos -> cantidad = 0;
¿Entiendes por qué se usa el operador flecha?: la variable grupoNaufragos es un puntero, ası́ que
hemos de acceder a la información apuntada antes de acceder al campo cantidad . Podrı́amos
haber escrito esa misma lı́nea ası́:
(* grupoNaufragos ).cantidad = 0;
pero hubiera resultado más incómodo (e ilegible). A continuación, la función repite cantidad
veces la acción consistente en seleccionar una fila y columna al azar (mediante la función rand
de stdlib.h) y lo anota en una posición del vector de náufragos. Puede que esta lı́nea te resulte
un tanto difı́cil de entender:
grupoNaufragos->naufrago[ grupoNaufragos->cantidad ].fila = fila;
pero no lo es tanto si la analizas paso a paso. Veamos. Empecemos por el ı́ndice que hemos
sombreado arriba. La primera vez, es 0, la segunda 1, y ası́ sucesivamente. En aras de comprender
la sentencia, nos conviene reescribir la sentencia poniendo de momento un 0 en el ı́ndice:
grupoNaufragos->naufrago[ 0 ].fila = fila;
Más claro, ¿no? Piensa que grupoNaufragos->naufrago es un vector como cualquier otro, ası́
que la expresión grupoNaufragos->naufrago[0] accede a su primer elemento. ¿De qué tipo es
ese elemento? De tipo struct Naufrago. Un elemento de ese tipo tiene un campo fila y se
accede a él con el operador punto. O sea, esa sentencia asigna el valor de fila al campo fila
de un elemento del vector naufrago del registro que es apuntado por grupoNaufragos. El resto
de la función te debe resultar fácil de leer ahora. Volvamos a la cuestión principal: ¿por qué
está mal diseñada esa función? Fácil: porque puede ubicar dos náufragos en la misma casilla
del tablero. ¿Cómo corregimos el problema? Asegurándonos de que cada náufrago ocupa una
casilla diferente. Tenemos dos posibilidades:
Generar la posición de cinco náufragos al azar y comprobar que son todas diferentes entre
sı́. Si lo son, perfecto: hemos acabado; si no, volvemos a repetir todo el proceso.
Ir generando la posición de cada náufrago de una en una y comprobando cada vez que
ésta es distinta de la de todos los náufragos anteriores. Si no lo es, volvemos a generar la
posición de este náufrago concreto; si lo es, pasamos al siguiente.
La segunda resulta más sencilla de implementar y es, a la vez, más eficiente. Aquı́ la tienes
implementada:
void pon_naufragos(struct GrupoNaufragos * grupoNaufragos, int cantidad )
/* Sitúa aleatoriamente cantidad náufragos en la estructura grupoNaufragos. */
{
int fila, columna, ya_hay_uno_ahi, i;
grupoNaufragos->cantidad = 0;
while (grupoNaufragos->cantidad != cantidad ) {
fila = rand () % FILAS;
columna = rand () % COLUMNAS;
ya_hay_uno_ahi = 0;
Nos vendrá bien disponer de una función que muestre por pantalla la ubicación y estado de
cada náufrago. Esta función no resulta útil para el juego (pues perderı́a toda la gracia), pero
sı́ para ayudarnos a depurar el programa. Podrı́amos, por ejemplo, ayudarnos con llamadas a
esa función mientras jugamos partidas de prueba y, una vez dado por bueno el programa, no
llamarla más. En cualquier caso, aquı́ la tienes:
void muestra_naufragos(struct GrupoNaufragos grupoNaufragos)
/* Muestra por pantalla las coordenadas de cada náufrago e informa de si sigue perdido.
* Útil para depuración del programa.
*/
{
int i;
char coordenadas[3];
La función está bien, pero podemos mejorarla. Fı́jate en cómo pasamos su parámetro: por valor.
¿Por qué? Porque no vamos a modificar su valor en el interior de la función. En principio, la
decisión de pasarlo por valor está bien fundamentada. No obstante, piensa en qué ocurre cada
vez que llamamos a la función: como un registro de tipo struct GrupoNaufragos ocupa 64
bytes (haz cuentas y compruébalo), cada llamada a la función obliga a copiar 64 bytes en la
pila. El problema se agravarı́a si en lugar de trabajar con un número máximo de 5 náufragos lo
hiciéramos con una cantidad mayor. ¿Es realmente necesario ese esfuerzo? La verdad es que no:
podemos limitarnos a copiar 4 bytes si pasamos una referencia al registro. Esta nueva versión
de la función efectúa el paso por referencia:
void muestra_naufragos( struct GrupoNaufragos * grupoNaufragos)
/* Muestra por pantalla las coordenadas de cada náufrago e informa de si sigue perdido.
* Útil para depuración del programa.
*/
{
int i, fila, columna;
char coordenadas[3];
Es posible usar el adjetivo const para dejar claro que pasamos el puntero por eficiencia,
pero no porque vayamos a modificar su contenido:
void muestra_naufragos( const struct GrupoNaufragos * grupoNaufragos)
3 int main(void)
4 {
5 struct GrupoNaufragos losNaufragos;
6
7 pon_naufragos(&losNaufragos, 5);
8 muestra_naufragos(&losNaufragos);
9
10 return 0;
11 }
¡Eh! ¡Se han ubicado en las mismas posiciones! ¿Qué gracia tiene el juego si en todas las
partidas aparecen los náufragos en las mismas casillas? ¿Cómo es posible que ocurra algo ası́?
¿No se generaba su ubicación al azar? Sı́ y no. La función rand genera números pseudoaleatorios.
Utiliza una fórmula matemática que genera una secuencia de números de forma tal que no
podemos efectuar una predicción del siguiente (a menos que conozcamos la fórmula, claro está).
La secuencia de números se genera a partir de un número inicial: la semilla. En principio, la
semilla es siempre la misma, ası́ que la secuencia de números es, también, siempre la misma.
¿Qué hacer, pues, si queremos obtener una diferente? Una posibilidad es solicitar al usuario el
valor de la semilla, que se puede modificar con la función srand , pero no parece lo adecuado
para un juego de ordenador (el usuario podrı́a hacer trampa introduciendo siempre la misma
semilla). Otra posibilidad es inicializar la semilla con un valor aleatorio. ¿Con un valor aleatorio?
Tenemos un pez que se muerde la cola: ¡resulta que necesito un número aleatorio para generar
números aleatorios! Mmmmm. Tranquilo, hay una solución: consultar el reloj del ordenador y
usar su valor como semilla. La función time (disponible incluyendo time.h) nos devuelve el
número de segundos transcurridos desde el inicio del dı́a 1 de enero de 1970 (lo que se conoce
por tiempo de la era Unix) y, naturalmente, es diferente cada vez que lo llamamos para iniciar
una partida. Aquı́ tienes la solución:
1 ...
2 #include <time.h>
3 ...
4
5 int main(void)
6 {
7 struct GrupoNaufragos losNaufragos;
8
9 srand (time(0));
10
11 pon_naufragos(&losNaufragos, 5);
12 muestra_naufragos(&losNaufragos);
13
14 return 0;
15 }
modificará el tablero de juego sustituyendo los sı́mbolos ’+’ por ’.’ en las direcciones
cardinales desde el punto de lanzamiento de la sonda,
1 ...
2 #define NO_SONDEADA ’+’
3 #define RESCATADO ’X’
4 #define SONDEADA ’.’
5 ...
6
16 // Recorrer la vertical
17 for (i=0; i<FILAS; i++) {
18 if ( hay_naufrago(i, columna, grupoNaufragos) )
19 detectados++;
20 if (tablero[i][columna] == NO_SONDEADA)
21 tablero[i][columna] = SONDEADA;
22 }
23
24 // Recorrer la horizontal
25 for (i=0; i<COLUMNAS; i++) {
26 if ( hay_naufrago(fila, i, grupoNaufragos) )
27 detectados++;
28 if (tablero[fila][i] == NO_SONDEADA)
29 tablero[fila][i] = SONDEADA;
30 }
31
Esta función se ayuda con otras dos: hay_naufrago y rescate. La primera nos indica si hay
un náufrago en una casilla determinada:
1 int hay_naufrago(int fila, int columna, const struct GrupoNaufragos * grupoNaufragos)
2 /* Averigua si hay un náufrago perdido en las coordenadas (fila, columna).
3 * Si lo hay devuelve 1; si no lo hay, devuelve 0.
4 */
6 {
7 int i;
8
8 srand (time(0));
9
10 pon_naufragos(&losNaufragos, 5);
11 inicializa_tablero(espacio);
12 muestra_tablero(espacio);
13
14 while ( ??? ) {
24 return 0;
25 }
¿Cuándo debe finalizar el bucle while exterior? Bien cuando hayamos rescatado a todos los
náufragos, bien cuando nos hayamos quedado sin sondas. En el primer caso habremos vencido
y en el segundo habremos perdido:
1 ...
2 #define SONDAS 20
3 ...
4
16 ...
17
18 int main(void)
19 {
20 char espacio[FILAS][COLUMNAS];
21 struct GrupoNaufragos losNaufragos;
22 int sondas_disponibles = SONDAS;
23 char coordenadas[TALLACAD+1];
24 int fila, columna;
25
26 srand (time(0));
27
28 pon_naufragos(&losNaufragos, 5);
29 inicializa_tablero(espacio);
30 muestra_tablero(espacio);
31
45 if (perdidos(&losNaufragos) == 0)
46 printf ("Has ganado. Puntuación: %d puntos.\n", SONDAS - sondas_disponibles);
47 else
48 printf ("Has perdido. Por tu culpa han muerto %d náufragos\n",
49 perdidos(&losNaufragos));
50
51 return 0;
52 }
Hemos definido una nueva función, perdidos, que calcula el número de náufragos que per-
manecen perdidos.
Y ya está. Te mostramos finalmente el listado completo del programa:
minigalaxis.c minigalaxis.c
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <string.h>
4 #include <ctype.h>
5 #include <time.h>
6
7 #define FILAS 9
8 #define COLUMNAS 20
9 #define TALLACAD 80
10 #define MAX_NAUFRAGOS 5
11 #define SONDAS 20
12
17 /**********************************************************
18 * Conversión entre los dos modos de expresar coordenadas
19 **********************************************************/
21
55 /****************************************
56 * Náufragos
57 ****************************************/
59
60 struct Naufrago {
61 int fila, columna; // Coordenadas
?
62 int encontrado; // Ha sido encontrado ya?
63 };
64
65 struct GrupoNaufragos {
66 struct Naufrago naufrago[MAX_NAUFRAGOS];
67 int cantidad ;
68 };
69
75 grupoNaufragos->cantidad = 0;
76 while (grupoNaufragos->cantidad != cantidad ) {
77 fila = rand () % FILAS;
78 columna = rand () % COLUMNAS;
79 ya_hay_uno_ahi = 0;
80 for (i=0; i<grupoNaufragos->cantidad ; i++)
81 if (fila == grupoNaufragos->naufrago[i].fila &&
82 columna == grupoNaufragos->naufrago[i].columna) {
83 ya_hay_uno_ahi = 1;
84 break;
85 }
86 if (!ya_hay_uno_ahi) {
87 grupoNaufragos->naufrago[grupoNaufragos->cantidad ].fila = fila;
88 grupoNaufragos->naufrago[grupoNaufragos->cantidad ].columna = columna;
89 grupoNaufragos->naufrago[grupoNaufragos->cantidad ].encontrado = 0;
90 grupoNaufragos->cantidad ++;
91 }
92 }
93 }
94
110
154 /****************************************
155 * Tablero
156 ****************************************/
158
187 /****************************************
188 * Sonda
189 ****************************************/
191
253 if (perdidos(&losNaufragos) == 0)
254 printf ("Has ganado. Puntuación: %d puntos.\n", SONDAS - sondas_disponibles);
255 else
256 printf ("Has perdido. Por tu culpa han muerto %d náufragos\n",
257 perdidos(&losNaufragos));
258
259 return 0;
260 }
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 202 Reescribe el programa para que no se use un variable de tipo struct GrupoNaufragos
como almacén del grupo de náufragos, sino una matriz paralela a la matriz espacio.
Cada náufrago se representará con un ’*’ mientras permanezca perdido, y con un ’X’
cuando haya sido descubierto.
· 203 Siempre que usamos rand en miniGalaxis calculamos un par de números aleatorios.
Hemos definido un nuevo tipo y una función:
1 struct Casilla {
2 int fila, columna;
3 };
4
7 grupoNaufragos->cantidad = 0;
8 while (grupoNaufragos->cantidad != cantidad ) {
9 una_casilla = casilla_al_azar ();
10 ya_hay_uno_ahi = 0;
11 for (i=0; i<grupoNaufragos->cantidad ; i++)
12 if ( una_casilla.fila == grupoNaufragos->naufrago[i].fila &&
13 una_casilla.columna == grupoNaufragos->naufrago[i].columna) {
14 ya_hay_uno_ahi = 1;
15 break;
16 }
17 if (!ya_hay_uno_ahi) {
18 grupoNaufragos->naufrago[grupoNaufragos->cantidad ].fila = una_casilla.fila ;
19 grupoNaufragos->naufrago[grupoNaufragos->cantidad ].columna = una_casilla.columna ;
20 grupoNaufragos->naufrago[grupoNaufragos->cantidad ].encontrado = 0;
21 grupoNaufragos->cantidad ++;
22 }
23 }
24 }
17 grupoNaufragos->cantidad = 0;
18 while (grupoNaufragos->cantidad != cantidad ) {
19 un_naufrago = naufrago_al_azar ();
20 ya_hay_uno_ahi = 0;
21 for (i=0; i<grupoNaufragos->cantidad ; i++)
22 if ( un_naufrago.fila == grupoNaufragos->naufrago[i].fila &&
23 un_naufrago.columna == grupoNaufragos->naufrago[i].columna) {
24 ya_hay_uno_ahi = 1;
25 break;
26 }
27 if (!ya_hay_uno_ahi) {
28 grupoNaufragos->naufrago[grupoNaufragos->cantidad ] = un_naufrago ;
29 grupoNaufragos->cantidad ++;
30 }
31 }
32 }
3.6. Recursión
Es posible definir funciones recursivas en C. La función factorial de este programa, por ejemplo,
define un cálculo recursivo del factorial:
factorial recursivo.c factorial recursivo.c
1 #include <stdio.h>
2
11 int main(void)
12 {
13 int valor ;
14
19 return 0;
20 }
factorial n 3
llamada desde lı́nea 8
factorial n 4
llamada desde lı́nea 8
factorial n 5
llamada desde lı́nea 17
main valor 5
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 207 Diseña una función que calcule recursivamente xn . La variable x será de tipo float y
n de tipo int.
· 208 Diseña una función recursiva que calcule el n-ésimo número de Fibonacci.
· 209 Diseña una función recursiva para calcular el número combinatorio n sobre m sabiendo
que
n
= 1,
n
n
= 1,
0
n n−1 n−1
= + .
m m m−1
· 210 Diseña un procedimiento recursivo llamado muestra_bin que reciba un número en-
tero positivo y muestre por pantalla su codificación en binario. Por ejemplo, si llamamos a
muestra_bin(5), por pantalla aparecerá el texto ((101)).
.............................................................................................
11 21 3 1 98 0 12 82 29 30 11 18 43 4 75 37
11 21 3 1 98 0 12 82 29 30 11 18 43 4 75 37
0 1 3 11 12 21 82 98
4 11 18 29 30 37 43 75
4. y ahora ((fundimos)) ambos vectores ordenados, obteniendo ası́ un único vector ordenado:
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
0 1 3 4 11 11 12 18 21 29 30 37 43 75 82 98
11 21 3 1 98 0 12 82 29 30 11 18 43 4 75 37
11 21 3 1 98 0 12 82 29 30 11 18 43 4 75 37
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
0 1 3 11 12 21 82 98 4 11 18 29 30 37 43 75
0 1 3 4 11 11 12 18 21 29 30 37 43 75 82 98
Está claro que hemos hecho ((trampa)): las lı́neas de trazo discontinuo esconden un proceso
complejo, pues la ordenación de cada uno de los vectores de 8 elementos supone la ordenación
(recursiva) de dos vectores de 4 elementos, que a su vez. . . ¿Cuándo acaba el proceso recursivo?
Cuando llegamos a un caso trivial: la ordenación de un vector que sólo tenga 1 elemento.
He aquı́ el proceso completo:
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
11 21 3 1 98 0 12 82 29 30 11 18 43 4 75 37
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
11 21 3 1 98 0 12 82 29 30 11 18 43 4 75 37
Divisiones
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
11 21 3 1 98 0 12 82 29 30 11 18 43 4 75 37
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
11 21 3 1 98 0 12 82 29 30 11 18 43 4 75 37
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
11 21 3 1 98 0 12 82 29 30 11 18 43 4 75 37
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
11 21 1 3 0 98 12 82 29 30 11 18 4 43 37 75
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
1 3 11 21 0 12 82 98 11 18 29 30 4 37 43 75
Fusiones
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
0 1 3 11 12 21 82 98 4 11 18 29 30 37 43 75
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
0 1 3 4 11 11 12 18 21 29 30 37 43 75 82 98
Nos queda por estudiar con detalle el proceso de fusión. Desarrollemos primero una función
que recoja la idea básica de la ordenación por fusión: se llamará mergesort y recibirá un vector
v y, en principio, la talla del vector que deseamos ordenar. Esta función utilizará una función
auxiliar merge encargada de efectuar la fusión de vectores ya ordenados. Aquı́ tienes un borrador
incompleto:
1 void mergesort(int v[], int talla)
2 {
3 if (talla == 1)
4 return;
5 else {
6 mergesort ( la primera mitad de v );
7 mergesort ( la segunda mitad de v );
8 merge( la primera mitad de v , la segunda mitad de v );
9 }
10 }
11 21 3 1 98 0 12 82 29 30 11 18 43 4 75 37
trabaje con ((subvectores)), es decir, con un vector e ı́ndices que señalan dónde empieza y dónde
acaba cada serie de valores.
Perfecto. Acabamos de expresar la idea de dividir un vector en dos sin necesidad de utilizar
nuevos vectores.
Nos queda por detallar la función merge. Dicha función recibe dos ((subvectores)) contiguos
ya ordenados y los funde, haciendo que la zona de memoria que ambos ocupan pase a estar
completamente ordenada. Este gráfico muestra cómo se fundirı́an, paso a paso, dos vectores, a
y b para formar un nuevo vector c. Necesitamos tres ı́ndices, i, j y k, uno para cada vector:
0 1 2 3 0 1 2 3 0 1 2 3 4 5 6 7
1 3 11 21 0 12 82 98
i j k
Inicialmente, los tres ı́ndices valen 0. Ahora comparamos a[i] con b[j], seleccionamos el menor
y almacenamos el valor en c[k]. Es necesario incrementar i si escogimos un elemento de a y j
si lo escogimos de b. En cualquier caso, hemos de incrementar también la variable k:
0 1 2 3 0 1 2 3 0 1 2 3 4 5 6 7
1 3 11 21 0 12 82 98 0
i j k
El proceso se repite hasta que alguno de los dos primeros ı́ndices, i o j, se ((sale)) del vector
correspondiente, tal y como ilustra esta secuencia de imágenes:
0 1 2 3 0 1 2 3 0 1 2 3 4 5 6 7
1 3 11 21 0 12 82 98 0 1
i j k
0 1 2 3 0 1 2 3 0 1 2 3 4 5 6 7
1 3 11 21 0 12 82 98 0 1 3
i j k
0 1 2 3 0 1 2 3 0 1 2 3 4 5 6 7
1 3 11 21 0 12 82 98 0 1 3 11
i j k
0 1 2 3 0 1 2 3 0 1 2 3 4 5 6 7
1 3 11 21 0 12 82 98 0 1 3 11 12
i j k
0 1 2 3 0 1 2 3 0 1 2 3 4 5 6 7
1 3 11 21 0 12 82 98 0 1 3 11 12 21
i j k
Ahora, basta con copiar los últimos elementos del otro vector al final de c:
0 1 2 3 0 1 2 3 0 1 2 3 4 5 6 7
1 3 11 21 0 12 82 98 0 1 3 11 12 21 82
i j k
0 1 2 3 0 1 2 3 0 1 2 3 4 5 6 7
1 3 11 21 0 12 82 98 0 1 3 11 12 21 82 98
i j k
Un último paso del proceso de fusión deberı́a copiar los elementos de c en a y b, que en realidad
son fragmentos contiguos de un mismo vector.
Vamos a por los detalles de implementación. No trabajamos con dos vectores independientes,
sino con un sólo vector en el que se marcan ((subvectores)) con pares de ı́ndices.
1 void merge(int v[], int inicio1, int final 1, int inicio2, int final 2)
2 {
3 int i, j, k;
4 int c[final 2-inicio1+1]; // Vector de talla determinada en tiempo de ejecución.
5
6 i = inicio1;
7 j = inicio2;
8 k = 0;
9
16 while (i<=final 1)
17 c[k++] = v[i++];
18
19 while (j<=final 2)
20 c[k++] = v[j++];
21
El último paso del procedimiento se encarga de copiar los elementos de c en el vector original.
Ya está. Bueno, aún podemos efectuar una mejora para reducir el número de parámetros:
fı́jate en que inicio2 siempre es igual a final 1+1. Podemos prescindir de uno de los dos parámetros:
6 i = inicio1;
7 j = final 1+1 ;
8 k = 0;
9
16 while (i<=final 1)
17 c[k++] = v[i++];
18
19 while (j<=final 2)
20 c[k++] = v[j++];
21
ordena.c ordena.c
1 #include <stdio.h>
2
10 i = inicio1;
11 j = final 1+1;
12 k = 0;
13
20 while (i<=final 1)
21 c[k++] = v[i++];
22
23 while (j<=final 2)
24 c[k++] = v[j++];
25
41 int main(void)
42 {
43 int mivector [TALLA];
44 int i, talla;
45
46 talla = 0;
47 for (i=0; i<TALLA; i++) {
48 printf ("Introduce elemento %d (negativo para acabar): ", i);
49 scanf ("%d", &mivector [i]);
50 if (mivector [i] < 0)
51 break;
52 talla++;
53 }
54
55 mergesort(mivector , 0, talla-1);
56
Mergesort y el estilo C
Los programadores C tienden a escribir los programas de una forma muy compacta. Estudia
esta nueva versión de la función merge:
Observa que los bucles for aceptan más de una inicialización (separándolas por comas)
y permiten que alguno de sus elementos esté en blanco (en el primer for la acción de
incremento del ı́ndice en blanco). No te sugerimos que hagas tú lo mismo: te prevenimos
para que estés preparado cuando te enfrentes a la lectura de programas C escritos por otros.
También vale la pena apreciar el uso del operador ternario para evitar una estructura
condicional if -else que en sus dos bloques asigna un valor a la misma celda del vector. Es
una práctica frecuente y da lugar, una vez acostumbrado, a programas bastante legibles.
La primera lı́nea es una declaración anticipada de la función impar , pues se usa antes de haber
sido definida. Con la declaración anticipada hemos ((adelantado)) la información acerca de qué
tipo de valores aceptará y devolverá la función.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 211 Dibuja el estado de la pila cuando se llega al caso base en la llamada recursiva impar (7).
.............................................................................................
La declaración anticipada resulta necesaria para programas con recursión indirecta, pero
también la encontrarás (o usarás) en programas sin recursión. A veces conviene definir funciones
en un orden que facilite la lectura del programa, y es fácil que se defina una función después
de su primer uso. Pongamos por caso el programa ordena.c en el que hemos implementado
el método de ordenación por fusión: puede que resulte más legible definir primero mergesort y
después merge pues, a fin de cuentas, las hemos desarrollado en ese orden. De definirlas ası́,
necesitarı́amos declarar anticipadamente merge:
ordena.c
1 #include <stdio.h>
2
5 void merge(int v[], int inicio1, int final 1, int final 2) ; // Declaración anticipada.
6
18 void merge(int v[], int inicio1, int final 1, int final 2) // Y ahora se define.
19 {
20 ...
3.7. Macros
El preprocesador permite definir un tipo especial de funciones que, en el fondo, no lo son: las
macros. Una macro tiene parámetros y se usa como una función cualquiera, pero las llamadas
no se traducen en verdaderas llamadas a función. Ahora verás por qué.
Vamos con un ejemplo:
1 int f (int y)
2 {
3 return 1 + g(y);
4 }
5
6 float g(float x)
7 {
8 return x*x;
9 }
3 int f (int y)
4 {
5 return 1 + g(y);
6 }
7
8 float g(float x)
9 {
10 return x*x;
11 }
La directiva con la que se define una macro es #define, la misma con la que declarábamos
constantes. La diferencia está en que la macro lleva uno o más parámetros (separados por
comas) encerrados entre paréntesis. Este programa define y usa la macro CUADRADO:
1 #include <stdio.h>
2
Por regla general, son más rápidas que las funciones, pues al no implicar una llamada
a función en tiempo de ejecución nos ahorramos la copia de argumentos en pila y el
salto/retorno a otro lugar del programa.
No obligan a dar información de tipo acerca de los parámetros ni del valor de retorno. Por
ejemplo, esta macro devuelve el máximo de dos números, sin importar que sean enteros
o flotantes:
La definición de la macro debe ocupar, en principio, una sola lı́nea. Si ocupa más de una
lı́nea, hemos de finalizar todas menos la última con el carácter ((\)) justo antes del salto
de lı́nea. Incómodo.
No admiten recursión.
Son peligrosı́simas. ¿Qué crees que muestra por pantalla este programa?:
1 #include <stdio.h>
2
¿36?, es decir, ¿el cuadrado de 6? Pues no es eso lo que obtienes, sino 15. ¿Por qué? El
preprocesador sustituye el fragmento CUADRADO(3+3) por. . . ¡3+3*3+3!
El resultado es, efectivamente, 15, y no el que esperábamos. Puedes evitar este problema
usando paréntesis:
1 #include <stdio.h>
2
5 main (void)
6 {
7 printf ("El cuadrado de 6 es %d\n", CUADRADO(3+3) );
8 return 0;
9 }
1 #include <stdio.h>
2
5 ...
7 No del todo cierto, pero no entraremos en detalles.
la variable se incrementa 2 veces, y no una sóla. Ten en cuenta que el compilador traduce lo
que ((ve)), y ((ve)) esto:
1 i = 3;
2 z = ((i++)*(i++));
8 int main(void)
9 {
10 int i;
11
15 return 0;
16 }
no se genera código de máquina con 10 llamadas a la función doble. El código de máquina que
se genera es virtualmente idéntico al que se genera para este otro programa equivalente:
1 #include <stdio.h>
2
3 int main(void)
4 {
5 int i;
6
10 return 0;
11 }
3 int turno(void)
4 {
5 static int contador = 0;
6
10 int main(void)
11 {
12 int i;
13
Si ejecutas el programa aparecerán por pantalla los números del 0 al 9. Con cada llamada,
contador devuelve su valor y se incrementa en una unidad, sin olvidar su valor entre llamada y
llamada.
La inicialización de las variables static es opcional: el compilador asegura que empiezan
valiendo 0.
Vamos a volver a escribir el programa que presentamos en el ejercicio 169 para generar
números primos consecutivos. Esta vez, vamos a hacerlo sin usar una variable global que re-
cuerde el valor del último primo generado. Usaremos en su lugar una variable local static:
primos.c primos.c
1 #include <stdio.h>
2
3 int siguienteprimo(void)
4 {
5 static int ultimoprimo = 0;
6 int esprimo;
7 int i;
8
9 do {
10 ultimoprimo++;
11 esprimo = 1;
12 for (i=2; i<ultimoprimo/2; i++)
13 if (ultimoprimo % i == 0) {
14 esprimo = 0;
15 break;
16 }
17 } while (!esprimo);
18 return ultimoprimo;
19 }
20
21 int main(void)
22 {
23 int i ;
24
Mucho mejor. Si puedes evitar el uso de variables globales, evı́talo. Las variables locales static
pueden ser la solución en bastantes casos.
6 s = 0.0;
7 x = a;
8 for (i=0; i<n; i++) {
9 s += x*x * (b-a)/n;
10 x += (b-a)/n;
11 }
12 return s;
13 }
14
20 s = 0.0;
21 x = a;
22 for (i=0; i<n; i++) {
23 s += x*x*x * (b-a)/n;
24 x += (b-a)/n;
25 }
26 return s;
27 }
Las dos funciones que hemos definido son básicamente iguales. Sólo difieren en su identificador
y en la función matemática que integran. ¿No serı́a mejor disponer de una única función C,
digamos integra, a la que suministremos como parámetro la función matemática que queremos
integrar? C lo permite:
6 s = 0.0;
7 x = a;
8 for (i=0; i<n; i++) {
9 s += f (x) * (b-a)/n;
10 x += (b-a)/n;
11 }
12 return s;
13 }
Hemos declarado un cuarto parámetro que es de tipo puntero a función. Cuando llamamos a
integra, el cuarto parámetro puede ser el identificador de una función que reciba un float y
devuelva un float:
integra.c integra.c
1 #include <stdio.h>
2
8 s = 0.0;
9 x = a;
10 for (i=0; i<n; i++) {
11 s += f (x) * (b-a)/n;
12 x += (b-a)/n;
13 }
14 return s;
15 }
16
17 float cuadrado(float x)
18 {
19 return x*x;
20 }
21
22 float cubo(float x)
23 {
24 return x*x*x;
25 }
26
27 int main(void)
28 {
29 printf ("Integral 1: %f\n", integra(0.0, 1.0, 10, cuadrado ));
30 printf ("Integral 2: %f\n", integra(0.0, 1.0, 10, cubo ));
31 return 0;
32 }
La forma en que se declara un parámetro del tipo ((puntero a función)) resulta un tanto
complicada. En nuestro caso, lo hemos declarado ası́: float (*f )(float). El primer float indica
que la función devuelve un valor de ese tipo. El (*f ) indica que el parámetro f es un puntero a
función. Y el float entre paréntesis indica que la función trabaja con un parámetro de tipo float.
Si hubiésemos necesitado trabajar con una función que recibe un float y un int, hubiésemos
escrito float (*f )(float, int) en la declaración del parámetro.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 216 ¿Puedes usar la función integra para calcular la integral definida de la función ma-
temática sin(x)? ¿Cómo?
siendo f una función matemática cualquiera que recibe un entero y devuelve un entero.
· 218 Diseña una función C capaz de calcular
b X
X d
f (i, j),
i=a j=c
siendo f una función matemática cualquiera que recibe dos enteros y devuelve un entero.
.............................................................................................
El programa principal se escribirá en otro fichero llamado principal.c. Dicho programa lla-
mará a la función maximo:
3 int main(void)
4 {
5 int x, y;
6
5 int main(void)
6 {
7 int x, y;
8
La compilación necesita tres pasos: uno por cada unidad de compilación y otro para enlazar.
1. El primer paso (gcc extremos.c -c) traduce a código de máquina el fichero o unidad de
compilación extremos.c. La opción -c indica al compilador que extremos.c es un módulo
y no define a la función main. El resultado de la compilación se deja en un fichero llamado
extremos.o. La extensión ((.o)) abrevia el término ((object code)), es decir, ((código objeto)).
Los ficheros con extensión ((.o)) contienen el código de máquina de nuestras funciones8 ,
pero no es directamente ejecutable.
2. El segundo paso (gcc principal.c -c) es similar al primero y genera el fichero principal.o
a partir de principal.c.
8. . . pero no sólo eso: también contienen otra información, como la denominada tabla de sı́mbolos.
Paso 3
Enlazador principal
Paso 2
principal.c Compilador principal.o
Puedes ahorrarte un paso fundiendo los dos últimos en uno sólo. Ası́:
$ gcc extremos.c -c
$ gcc principal.c extremos.o -o principal
Este diagrama muestra todos los pasos del proceso a los que aludimos:
Paso 1
extremos.c Compilador extremos.o
Paso 2
Para conseguir un programa ejecutable es necesario que uno de los módulos (¡pero sólo uno
de ellos!) defina una función main. Si ningún módulo define main o si main se define en más
de un módulo, el enlazador protestará y no generará fichero ejecutable alguno.
Para incluir la cabecera en nuestro programa, escribiremos una nueva directiva #include:
principal.c principal.c
1 #include <stdio.h>
2 #include "extremos.h"
3
4 int main(void)
5 {
6 int x, y;
7
Documentación y cabeceras
Es importante que documentes bien los ficheros de cabecera, pues es frecuente que los
programadores que usen tu módulo lo consulten para hacerse una idea de qué ofrece.
Nuestro módulo podrı́a haberse documentado ası́:
extremos.h
1 /*******************************************************
2 * Módulo: extremos
3 *
4 * Propósito: funciones para cálculo de valores máximos
5 * y mı́nimos.
6 *
7 * Autor: A. U. Thor.
8 *
9 * Fecha: 12 de enero de 1997
10 *
11 * Estado: Incompleto. Falta la función minimo.
12 *******************************************************/
14
La única diferencia con respecto a otros #include que ya hemos usado estriba en el uso de
comillas dobles para encerrar el nombre del fichero, en lugar de los caracteres ((<)) y ((>)). Con
ello indicamos al preprocesador que el fichero extremos.h se encuentra en nuestro directorio
activo. El preprocesador se limita a sustituir la lı́nea en la que aparece #include "extremos.h"
por el contenido del fichero. En un ejemplo tan sencillo no hemos ganado mucho, pero si el
módulo extremos.o contuviera muchas funciones, con sólo una lı́nea habrı́amos conseguido
((importarlas)) todas.
Aquı́ tienes una actualización del gráfico que muestra el proceso completo de compilación:
extremos.h
Bibliotecas
Ya has usado funciones y datos predefinidos, como las funciones y las constantes ma-
temáticas. Hemos hablado entonces del uso de la biblioteca matemática. ¿Por qué
((biblioteca)) y no ((módulo))? Una biblioteca es más que un módulo: es un conjunto de
módulos.
Cuando se tiene una pléyade de ficheros con extensión ((.o)), conviene empaquetarlos en
uno solo con extensión ((.a)) (por ((archive))). Los ficheros con extensión ((.a)) son similares
a los ficheros con extensión ((.tar)): meras colecciones de ficheros. De hecho, ((tar)) (tape
archiver ) es una evolución de ((.ar)) (por ((archiver))), el programa con el que se manipulan
los ficheros con extensión ((.a)).
La biblioteca matemática, por ejemplo, agrupa un montón de módulos. En un sistema
Linux se encuentra en el fichero /usr/lib/libm.a y puedes consultar su contenido con esta
orden:
$ ar tvf /usr/lib/libm.a
rw-r--r-- 0/0 29212 Sep 9 18:17 2002 k_standard.o
rw-r--r-- 0/0 8968 Sep 9 18:17 2002 s_lib_version.o
rw-r--r-- 0/0 9360 Sep 9 18:17 2002 s_matherr.o
rw-r--r-- 0/0 8940 Sep 9 18:17 2002 s_signgam.o
.
.
.
rw-r--r-- 0/0 1152 Sep 9 18:17 2002 slowexp.o
rw-r--r-- 0/0 1152 Sep 9 18:17 2002 slowpow.o
Como puedes ver, hay varios ficheros con extensión ((.o)) en su interior. (Sólo te mostra-
mos el principio y el final del resultado de la llamada, pues hay un total de ¡395 ficheros!)
Cuando usas la biblioteca matemática compilas ası́:
$ gcc programa.c -lm -o programa
o, equivalentemente, ası́:
$ gcc programa.c /usr/lib/libm.a
Si deseamos que otras unidades de compilación puedan acceder a esa variable, tendremos que
incluir su declaración en la cabecera. ¿Cómo? Una primera idea es poner, directamente, la
declaración ası́:
E mimodulo.h E
1 int variable;
De ese modo, cuando se compila un programa que incluye a mimodulo.h, el compilador sabe
que variable es de tipo int y que está definida en alguna unidad de compilación, por lo que no
la crea por segunda vez.
Ahora, los ficheros b.h y c.h incluyen a a.h y declaran la existencia de sendas funciones:
b.h
1 // Cabecera b.h
2 #include "a.h"
3
c.h
1 // Cabecera c.h
2 #include "a.h"
3
3 #include "b.h"
4
5 #include "c.h"
6
7 int main(void)
8 {
9 ...
10 }
El resultado es que el a.h acaba quedando incluido ¡dos veces! Tras el paso de programa.c por
el preprocesador, el compilador se enfrenta, a este texto:
programa.c
1 #include <stdio.h>
2
3 // Cabecera b.h.
4 // Cabecera a.h.
5 struct A {
6 int a;
7 };
8 // Fin de cabecera a.h.
9
13 // Cabecera c.h.
14 // Cabecera a.h.
15 struct A {
16 int a;
17 };
18 // Fin de cabecera a.h.
19
23 int main(void)
24 {
25 ...
26 }
El compilador encuentra, por tanto, la definición de struct A por duplicado, y nos avisa del
((error)). No importa que las dos veces se declare de la misma forma: C lo considera ilegal. El
problema puede resolverse reescribiendo a.h (y, en general, cualquier fichero cabecera) ası́:
1 // Cabecera de a.h
2 #ifndef A_H
3 #define A_H
4
5 struct A {
6 int a;
7 };
8
9 #endif
10 // Fin de cabecera de a.h
Las directivas #ifndef /#endif marcan una zona de ((código condicional)). Se interpretan ası́:
((si la constante A_H no está definida, entonces incluye el fragmento hasta el #endif , en caso
contrario, sáltate el texto hasta el #endif )). O sea, el compilador verá o no lo que hay entre
las lı́neas 1 y 6 en función de si existe o no una determinada constante. No debes confundir
estas directivas con una sentencia if : no lo son. La sentencia if permite ejecutar o no un bloque
de sentencias en función de que se cumpla o no una condición en tiempo de ejecución. Las
directivas presentadas permiten que el compilador vea o no un fragmento arbitrario de texto
en función de si existe o no una constante en tiempo de compilación.
Observa que lo primero que se hace en ese fragmento de programa es definir la constante
A_H (lı́nea 3). La primera vez que se incluya la cabecera a.h no estará aún definida A_H, ası́ que
se incluirán las lı́neas 3–8. Uno de los efectos será que A_H pasará a estar definida. La segunda
vez que se incluya la cabecera a.h, A_H ya estará definida, ası́ que el compilador no verá por
segunda vez la definición de struct A.
El efecto final es que la definición de struct A sólo se ve una vez. He aquı́ lo que resulta de
programa.c tras su paso por el preprocesador:
programa.c
1 #include <stdio.h>
2
3 // Cabecera b.h.
4 // Cabecera a.h.
5 struct A {
6 int a;
7 };
8 // Fin de cabecera a.h.
9
13 // Cabecera c.h.
14 // Cabecera a.h.
15 // Fin de cabecera a.h.
16
20 int main(void)
21 {
22 ...
23 }
La segunda inclusión de a.h no ha supuesto el copiado del texto guardado entre directivas
#ifndef /#endif . Ingenioso, ¿no?
La Reina se puso congestionada de furia, y, tras lanzarle una mirada felina, empezó a
gritar: ((¡Que le corten la cabeza! ¡Que le corten. . . !)).
Vimos en el capı́tulo 2 que los vectores de C presentaban un serio inconveniente con respecto a
las listas de Python: su tamaño debı́a ser fijo y conocido en tiempo de compilación, es decir, no
podı́amos alargar o acortar los vectores para que se adaptaran al tamaño de una serie de datos
durante la ejecución del programa. C permite una gestión dinámica de la memoria, es decir,
solicitar memoria para albergar el contenido de estructuras de datos cuyo tamaño exacto no
conocemos hasta que se ha iniciado la ejecución del programa. Estudiaremos aquı́ dos formas
de superar las limitaciones de tamaño que impone el C:
mediante vectores cuyo tamaño se fija en tiempo de ejecución,
y mediante registros enlazados, también conocidos como listas enlazadas (o, simplemente,
listas).
Ambas aproximaciones se basan en el uso de punteros y cada una de ellas presenta diferentes
ventajas e inconvenientes.
3 int a[TALLA];
Pero, ¿y si no sabemos a priori cuántos elementos debe albergar el vector?1 Por lo estudiado
hasta el momento, podemos definir TALLA como el número más grande de elementos posible,
el número de elementos para el peor de los casos. Pero, ¿y si no podemos determinar un
número máximo de elementos? Aunque pudiéramos, ¿y si éste fuera tan grande que, en la
práctica, supusiera un despilfarro de memoria intolerable para situaciones normales? Imagina
una aplicación de agenda telefónica personal que, por si acaso, reserva 100000 entradas en un
vector. Lo más probable es que un usuario convencional no gaste más de un centenar. Estaremos
desperdiciando, pues, unas 99900 celdas del vector, cada una de las cuales puede consistir en
un centenar de bytes. Si todas las aplicaciones del ordenador se diseñaran ası́, la memoria
disponible se agotarı́a rapidı́simamente.
1 En la sección 3.5.3 vimos cómo definir vectores locales cuya talla se decide al ejecutar una función: lo que
denominamos ((vectores de longitud variable)). Nos proponemos dos objetivos: por una parte, poder redimensionar
vectores globales; y, por otro, vamos a permitir que un vector crezca y decrezca en tamaño cuantas veces
queramos. Los ((vectores de longitud variable)) que estudiamos en su momento son inapropiados para cualquiera
de estos dos objetivos.
malloc (abreviatura de ((memory allocate)), que podemos traducir por ((reservar memo-
ria))): solicita un bloque de memoria del tamaño que se indique (en bytes);
free (que en inglés significa ((liberar))): libera memoria obtenida con malloc, es decir, la
marca como disponible para futuras llamadas a malloc.
4 int main(void)
5 {
6 int * a;
7 int talla, i;
8
16 return 0;
17 }
Fı́jate en cómo se ha definido el vector a (lı́nea 6): como int * a, es decir, como puntero a entero.
No te dejes engañar: no se trata de un puntero a un entero, sino de un puntero a una secuencia de
enteros. Ambos conceptos son equivalentes en C, pues ambos son meras direcciones de memoria.
La variable a es un vector dinámico de enteros, pues su memoria se obtiene dinámicamente,
esto es, en tiempo de ejecución y según convenga a las necesidades. No sabemos aún cuántos
enteros serán apuntados por a, ya que el valor de talla no se conocerá hasta que se ejecute el
programa y se lea por teclado.
Sigamos. La lı́nea 10 reserva memoria para talla enteros y guarda en a la dirección de
memoria en la que empiezan esos enteros. La función malloc presenta un prototipo similar a
éste:
stdlib.h
...
void * malloc(int bytes);
...
Es una función que devuelve un puntero especial, del tipo de datos void *. ¿Qué significa
void *? Significa ((puntero a cualquier tipo de datos)), o sea, ((dirección de memoria)), sin más.
La función malloc no se usa sólo para reservar vectores dinámicos de enteros: puedes reservar
con ella vectores dinámicos de cualquier tipo base. Analicemos ahora el argumento que pasamos
a malloc. La función espera recibir como argumento un número entero: el número de bytes que
queremos reservar. Si deseamos reservar talla valores de tipo int, hemos de solicitar memoria
para talla * sizeof (int) bytes. Recuerda que sizeof (int) es la ocupación en bytes de un dato
de tipo int (y que estamos asumiendo que es de 4).
Si el usuario decide que talla valga, por ejemplo, 5, se reservará un total de 20 bytes y la
memoria quedará ası́ tras ejecutar la lı́nea 10:
0 1 2 3 4
Aritmética de punteros
Una curiosidad: el acceso indexado a[0] es equivalente a *a. En general, a[i] es equivalente
a *(a+i), es decir, ambas son formas de expresar el concepto ((accede al contenido de la
dirección a con un desplazamiento de i veces el tamaño del tipo base)). La sentencia de
asignación a[i] = i podrı́a haberse escrito como *(a+i) = i. En C es posible sumar o restar
un valor entero a un puntero. El entero se interpreta como un desplazamiento dado en
unidades ((tamaño del tipo base)) (en el ejemplo, 4 bytes, que es el tamaño de un int). Es
lo que se conoce por aritmética de punteros.
La aritmética de punteros es un punto fuerte de C, aunque también tiene sus detractores:
resulta sencillo provocar accesos incorrectos a memoria si se usa mal.
Como ves, free recibe un puntero a cualquier tipo de datos: la dirección de memoria en la que
empieza un bloque previamente obtenido con una llamada a malloc. Lo que hace free es liberar
ese bloque de memoria, es decir, considerar que pasa a estar disponible para otras posibles
llamadas a malloc. Es como cerrar un fichero: si no necesito un recurso, lo libero para que otros
lo puedan aprovechar.2 Puedes aprovechar ası́ la memoria de forma óptima.
Recuerda: tu programa debe efectuar una llamada a free por cada llamada a malloc. Es muy
importante.
Conviene que después de hacer free asignes al puntero el valor NULL, especialmente si la
variable sigue ((viva)) durante bastante tiempo. NULL es una constante definida en stdlib.h. Si
un puntero vale NULL, se entiende que no apunta a un bloque de memoria. Gráficamente, un
puntero que apunta a NULL se representa ası́:
a
La función malloc puede fallar por diferentes motivos. Podemos saber cuándo ha fallado
porque malloc lo notifica devolviendo el valor NULL. Imagina que solicitas 2 megabytes de
memoria en un ordenador que sólo dispone de 1 megabyte. En tal caso, la función malloc
devolverá el valor NULL para indicar que no pudo efectuar la reserva de memoria solicitada.
2 Y, como en el caso de un fichero, si no lo liberas tú explı́citamente, se libera automáticamente al finalizar
la ejecución del programa. Aún ası́, te exigimos disciplina: oblı́gate a liberarlo tú mismo tan pronto dejes de
necesitarlo.
Los programas correctamente escritos deben comprobar si se pudo obtener la memoria so-
licitada y, en caso contrario, tratar el error.
1 a = malloc(talla * sizeof (int));
2 if (a == NULL) {
3 printf ("Error: no hay memoria suficiente\n");
4 }
5 else {
6 ...
7 }
Nuestros programas, sin embargo, no incluirán esta comprobación. Estamos aprendiendo a pro-
gramar y sacrificaremos las comprobaciones como ésta en aras de la legibilidad de los programas.
Pero no lo olvides: los programas con un acabado profesional deben comprobar y tratar posibles
excepciones, como la no existencia de suficiente memoria.
Fragmentación de la memoria
Ya hemos dicho que malloc puede fracasar si se solicita más memoria de la disponible en
el ordenador. Parece lógico pensar que en un ordenador con 64 megabytes, de los que el
sistema operativo y los programas en ejecución han consumido, digamos, 16 megabytes,
podamos solicitar un bloque de hasta 48 megabytes. Pero eso no está garantizado. Imagina
que los 16 megabytes ya ocupados no están dispuestos contiguamente en la memoria sino
que, por ejemplo, se alternan con fragmentos de memoria libre de modo que, de cada cuatro
megabytes, uno está ocupado y tres están libres, como muestra esta figura:
En tal caso, el bloque de memoria más grande que podemos obtener con malloc es de ¡sólo
tres megabytes!
Decimos que la memoria está fragmentada para referirnos a la alternancia de bloques
libres y ocupados que limita su disponibilidad. La fragmentación no sólo limita el máximo
tamaño de bloque que puedes solicitar, además, afecta a la eficiencia con la que se ejecutan
las llamadas a malloc y free.
También puedes usar NULL para inicializar punteros y dejar explı́citamente claro que no se
les ha reservado memoria.
4 int main(void)
5 {
6 int * a = NULL;
7 int talla, i;
8
16 return 0;
17 }
4 int main(void)
5 {
6 int * a = NULL;
7 int talla, i;
8 int * p;
9
17 return 0;
18 }
20 return pares;
21 }
Observa que devolvemos un dato de tipo int *, es decir, un puntero a entero; bueno, en realidad
se trata de un puntero a una secuencia de enteros (recuerda que son conceptos equivalentes en
C). Es la forma que tenemos de devolver vectores desde una función.
Este programa, por ejemplo, llama a selecciona_pares:
pares.c
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <time.h>
4
5 #define TALLA 10
6
.
.
.
27 }
28
29 int main(void)
30 {
31 int vector [TALLA], i;
32 int * seleccion;
33
47 free(seleccion);
48 seleccion = NULL;
49
50 return 0;
51 }
14 j = 0;
15 for (i=0; i<talla; i++)
16 if (a[i] % 2 == 0)
17 pares[j++] = a[i];
18
19 return pares;
20 }
5 #define TALLA 10
6
20 j = 0;
21 for (i=0; i<talla; i++)
22 if (a[i] % 2 == 0)
23 pares[j++] = a[i];
24
25 return pares;
26 }
27
28 int main(void)
29 {
30 int vector [TALLA], i;
31 int * seleccion, seleccionados;
32
47 free(seleccion);
48 seleccion = NULL;
49
50 return 0;
51 }
Por cierto, el prototipo de la función, que es éste:
int * selecciona_pares( int a[] , int talla, int * seleccionados);
puede cambiarse por este otro:
int * selecciona_pares( int * a , int talla, int * seleccionados);
Conceptualmente, es lo mismo un parámetro declarado como int a[] que como int * a: ambos
son, en realidad, punteros a enteros3 . No obstante, es preferible utilizar la primera forma cuando
un parámetro es un vector de enteros, ya que ası́ lo distinguimos fácilmente de un entero pasado
por referencia. Si ves el último prototipo, no hay nada que te permita saber si a es un vector o
un entero pasado por referencia como seleccionados. Es más legible, pues, la primera forma.
int * primeros(void)
{
int i, v[10];
for (i=0; i<10; i++)
v[i] = i + 1;
return v;
}
La función devuelve, a fin de cuentas, una dirección de memoria en la que empieza una
secuencia de enteros. Y es verdad: eso es lo hace. El problema radica en que la memoria a
la que apunta ¡no ((existe)) fuera de la función! La memoria que ocupa v se libera tan pronto
finaliza la ejecución de la función. Este intento de uso de la función, por ejemplo, trata de
acceder ilegalmente a memoria:
int main(void)
{
int * a;
a = primeros();
printf ("%d ", a[i]); // No existe a[i].
}
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 219 Diseña una función que seleccione todos los números positivos de un vector de enteros.
La función recibirá el vector original y un parámetro con su longitud y devolverá dos datos: un
puntero al nuevo vector de enteros positivos y su longitud. El puntero se devolverá como valor
de retorno de la función, y la longitud mediante un parámetro adicional (un entero pasado por
referencia).
· 220 Desarrolla una función que seleccione todos los números de un vector de float mayores
que un valor dado. Diseña un programa que llame correctamente a la función y muestre por
pantalla el resultado.
· 221 Escribe un programa que lea por teclado un vector de float cuyo tamaño se solicitará
previamente al usuario. Una vez leı́dos los componentes del vector, el programa copiará sus
valores en otro vector distinto que ordenará con el método de la burbuja. Recuerda liberar toda
memoria dinámica solicitada antes de finalizar el programa.
· 222 Escribe una función que lea por teclado un vector de float cuyo tamaño se solicitará
previamente al usuario. Escribe, además, una función que reciba un vector como el leı́do en la
función anterior y devuelva una copia suya con los mismos valores, pero ordenados de menor a
mayor (usa el método de ordenación de la burbuja o cualquier otro que conozcas).
Diseña un programa que haga uso de ambas funciones. Recuerda que debes liberar toda
memoria dinámica solicitada antes de finalizar la ejecución del programa.
· 223 Escribe una función que reciba un vector de enteros y devuelva otro con sus n mayores
valores, siendo n un número menor o igual que la talla del vector original.
3 En realidad, hay una pequeña diferencia. La declaración int a[] hace que a sea un puntero inmutable,
mientras que int * a permite modificar la dirección apuntada por a haciendo, por ejemplo, a++. De todos
modos, no haremos uso de esa diferencia en este texto.
· 224 Escribe una función que reciba un vector de enteros y un valor n. Si n es menor o igual
que la talla del vector, la función devolverá el un vector con las n primeras celdas del vector
original. En caso contrario, devolverá un vector de n elementos con un copia del contenido del
original y con valores nulos hasta completarlo.
.............................................................................................
No resulta muy elegante que una función devuelva valores mediante return y, a la vez, me-
diante parámetros pasados por referencia. Una posibilidad es usar únicamente valores pasados
por referencia:
pares 1.c pares.c
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <time.h>
4
5 #define TALLA 10
6
11 *numpares = 0;
12 for (i=0; i<talla; i++)
13 if (a[i] % 2 == 0)
14 (*numpares)++;
15
18 j = 0;
19 for (i=0; i<talla; i++)
20 if (a[i] % 2 == 0)
21 (*pares)[j++] = a[i];
22 }
23
24 int main(void)
25 {
26 int vector [TALLA], i;
27 int * seleccion , seleccionados;
28
29 srand (time(0));
30 for (i=0; i<TALLA; i++)
31 vector [i] = rand ();
32
38 free(seleccion);
39 seleccion = NULL;
40
41 return 0;
42 }
Las rutinas que nosotros diseñamos deberı́an presentar un comportamiento similar. La fun-
ción selecciona_pares, por ejemplo, podrı́a implementarse ası́:
5 *numpares = 0;
6 for (i=0; i<talla; i++)
7 if (a[i] % 2 == 0)
8 (*numpares)++;
9 *pares = malloc(*numpares * sizeof (int) );
10 if (*pares == NULL) { // Algo fue mal: no conseguimos la memoria.
11 *numpares = 0; // Informamos de que el vector tiene capacidad 0...
12 return 0; // y devolvemos el valor 0 para advertir de que hubo un error.
13 }
14 j = 0;
15 for (i=0; i<talla; i++)
16 if (a[i] % 2 == 0)
17 (*pares)[j++] = a[i];
18 return 1; // Si llegamos aquı́, todo fue bien, ası́ que avisamos de ello con el valor 1.
19 }
Hay que decir, no obstante, que esta forma de aviso de errores empieza a quedar obsoleto.
Los lenguajes de programación más modernos, como C++ o Python, suelen basar la detección
(y el tratamiento) de errores en las denominadas ((excepciones)).
Más elegante resulta definir un registro ((vector dinámico de enteros)) que almacene con-
juntamente tanto el vector de de elementos propiamente dicho como el tamaño del vector4 :
4 Aunque recomendemos este nuevo método para gestionar vectores de tamaño variable, has de saber, cuando
menos, leer e interpretar correctamente parámetros con tipos como int a[], int *a, int *a[] o int **a, pues
muchas veces tendrás que utilizar bibliotecas escritas por otros programadores o leer código fuente de programas
cuyos diseñadores optaron por estos estilos de paso de parámetros.
5 struct VectorDinamicoEnteros {
6 int * elementos; // Puntero a la zona de memoria con los elementos.
7 int talla; // Número de enteros almacenados en esa zona de memoria.
8 };
9
17 pares.talla = 0;
18 for (i=0; i<entrada.talla; i++)
19 if (entrada.elementos[i] % 2 == 0)
20 pares.talla++;
21
24 j = 0;
25 for (i=0; i<entrada.talla; i++)
26 if (pares.elementos[i] % 2 == 0)
27 pares.elementos[j++] = entrada.elementos[i];
28
29 return pares;
30 }
31
32 int main(void)
33 {
34 int i;
35 struct VectorDinamicoEnteros vector , seleccionados;
36
37 vector.talla = 10;
38 vector.elementos = malloc(vector.talla * sizeof (int));
39 srand (time(0));
40 for (i=0; i<vector.talla; i++)
41 vector.elementos[i] = rand ();
42
43 seleccionados = selecciona_pares(vector );
44
48 free(seleccionados.elementos);
49 seleccionados.elementos = NULL;
50 seleccionados.talla = 0;
51
52 return 0;
53 }
1 #include <stdio.h>
2 #include <stdlib.h>
3
4 struct VectorDinamicoEnteros {
5 int * elementos;
6 int talla;
7 };
8
14 pares->talla = 0;
15 for (i=0; i<entrada.talla; i++)
16 if (entrada.elementos[i] % 2 == 0)
17 pares->talla++;
18
21 j = 0;
22 for (i=0; i<entrada.talla; i++)
23 if (entrada.elementos[i] % 2 == 0)
24 pares->elementos[j++] = entrada.elementos[i];
25 }
26
27 int main(void)
28 {
29 int i;
30 struct VectorDinamicoEnteros vector , seleccionados;
31
32 vector.talla = 10;
33 vector.elementos = malloc(vector.talla * sizeof (int));
34 for (i=0; i<vector.talla; i++)
35 vector.elementos[i] = rand ();
36
37 selecciona_pares(vector , &seleccionados);
38
42 free(seleccionados.elementos);
43 seleccionados.elementos = NULL;
44 seleccionados.talla = 0;
45
46 return 0;
47 }
Como ves, tienes muchas soluciones técnicamente diferentes para realizar lo mismo. Deberás
elegir en función de la elegancia de cada solución y de su eficiencia.
struct Poligono {
struct Punto * p;
int puntos;
Listas Python
Empieza a quedar claro que Python es un lenguaje mucho más cómodo que C para gestionar
vectores dinámicos, que allı́ denominábamos listas. No obstante, debes tener presente que
el intérprete de Python está escrito en C, ası́ que cuando manejas listas Python estás,
indirectamente, usando memoria dinámica como malloc y free.
Cuando creas una lista Python con una orden como a = [0] * 5 o a = [0, 0, 0, 0, 0],
estás reservando espacio en memoria para 5 elementos y asignándole a cada elemento el
valor 0. La variable a puede verse como un simple puntero a esa zona de memoria (en
realidad es algo más complejo).
Cuando se pierde la referencia a una lista (por ejemplo, cambiando el valor asignado
a a), Python se encarga de detectar automáticamente que la lista ya no es apuntada por
nadie y de llamar a free para que la memoria que hasta ahora ocupaba pase a quedar libre.
};
Solicitamos memoria para pol.puntos celdas, cada una con capacidad para un dato de tipo
struct Punto (es decir, ocupando sizeof (struct Punto) bytes).
Nos vendrá bien una función que libere la memoria solicitada para almacenar un polı́gono,
ya que, de paso, pondremos el valor correcto en el campo puntos:
1 void libera_poligono(struct Poligono * pol )
2 {
3 free (pol ->p);
4 pol ->p = NULL;
5 pol ->puntos = 0;
6 }
Es importante que entiendas bien expresiones como pol.p[i].x. Esa, en particular, significa: del
parámetro pol , que es un dato de tipo struct Poligono, accede al componente i del campo p,
que es un vector de puntos; dicho componente es un dato de tipo struct Punto, pero sólo nos
interesa acceder a su campo x (que, por cierto, es de tipo float).
Juntemos todas las piezas y añadamos un sencillo programa principal que invoque a las
funciones desarrolladas:
polinomios dinamicos.c polinomios dinamicos.c
1 #include <stdio.h>
2 #include <stdlib.h>
3
4 struct Punto {
5 float x, y;
6 };
7
8 struct Poligono {
9 struct Punto * p;
10 int puntos;
11 };
12
48 int main(void)
49 {
50 struct Poligono un_poligono;
51 float perimetro;
52
53 un_poligono = lee_poligono();
54 perimetro = perimetro_poligono(un_poligono);
55 printf ("Perı́metro %f\n", perimetro);
56 libera_poligono(&un_poligono);
57
58 return 0;
59 }
4 struct Punto {
5 float x, y;
6 };
7
8 struct Poligono {
9 struct Punto * p;
10 int puntos;
11 };
12
47 int main(void)
48 {
49 struct Poligono un_poligono;
50 float perimetro;
51
52 lee_poligono(&un_poligono);
53 perimetro = perimetro_poligono(&un_poligono);
54 printf ("Perı́metro %f\n", perimetro);
55 libera_poligono(&un_poligono);
56
57 return 0;
58 }
En esta versión hemos optado, siempre que ha sido posible, por el paso de parámetros por
referencia, es decir, por pasar la dirección de la variable en lugar de una copia de su contenido.
Hay una razón para hacerlo: la eficiencia. Cada dato de tipo struct Poligono esta formado por
un puntero (4 bytes) y un entero (4 bytes), ası́ que ocupa 8 bytes. Si pasamos o devolvemos una
copia de un struct Poligono, estamos copiando 8 bytes. Si, por contra, pasamos su dirección de
memoria, sólo hay que pasar 4 bytes. En este caso particular no hay una ganancia extraordinaria,
pero en otras aplicaciones manejarás structs tan grandes que el paso de la dirección compensará
la ligera molestia de la notación de acceso a campos con el operador ->.
Puede que te extrañe el término const calificando el parámetro de perimetro_poligono. Su
uso es opcional y sirve para indicar que, aunque es posible modificar la información apuntada por
pol , no lo haremos. En realidad suministramos el puntero por cuestión de eficiencia, no porque
deseemos modificar el contenido. Con esta indicación conseguimos dos efectos: si intentásemos
modificar accidentalmente el contenido, el compilador nos advertirı́a del error; y, si fuera posible,
el compilador efectuarı́a optimizaciones que no podrı́a aplicar si la información apuntada por
pol pudiera modificarse en la función.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 225 ¿Funciona esta otra implementación de perimetro_poligono?
1 float perimetro_poligono(struct Poligono pol )
2 {
3 int i;
4 float perim = 0.0;
5
· 226 Diseña una función que cree un polı́gono regular de n lados inscrito en una circunfe-
rencia de radio r. Esta figura muestra un pentágono inscrito en una circunferencia de radio r y
las coordenadas de cada uno de sus vértices:
(r cos(2π/5), r sin(2π/5))
r
(r cos(0), r sin(0))
Como puedes ver, el campo p es un puntero a float, o sea, un vector dinámico de float. Diseña
y utiliza funciones que hagan lo siguiente:
Leer un polinomio por teclado. Se pedirá el grado del polinomio y, tras reservar memoria
suficiente para sus coeficientes, se pedirá también el valor de cada uno de ellos.
Sumar dos polinomios. Ten en cuenta que cada uno de ellos puede ser de diferente grado
y el resultado tendrá, en principio, grado igual que el mayor grado de los operandos. (Hay
excepciones; piensa cuáles.)
· 228 Diseña un programa que solicite la talla de una serie de valores enteros y dichos valores.
El programa ordenará a continuación los valores mediante el procedimiento mergesort. (Ten en
cuenta que el vector auxiliar que necesita merge debe tener capacidad para el mismo número
de elementos que el vector original.)
.............................................................................................
Con calloc, puedes pedir memoria para un vector de talla enteros ası́:
¿Por qué no usar siempre calloc, si parece mejor que malloc? Por eficiencia. En ocasiones
no desearás que se pierda tiempo de ejecución inicializando la memoria a cero, ya que tú
mismo querrás inicializarla a otros valores inmediatamente. Recuerda que garantizar la mayor
eficiencia de los programas es uno de los objetivos del lenguaje de programación C.
4 #define CAPACIDAD 80
5
6 int main(void)
7 {
8 char cadena1[CAPACIDAD+1], cadena2[CAPACIDAD+1];
9 char * cadena3;
10
13
16 strcpy(cadena3, cadena1);
17 strcat(cadena3, cadena2);
18
21 free(cadena3);
22 cadena3 = NULL;
23
24 return 0;
25 }
Como las dos primeras cadenas se leen con gets, hemos de definirlas como cadenas estáticas.
La tercera cadena reserva exactamente la misma cantidad de memoria que ocupa.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 229 Diseña una función que lea una cadena y construya otra con una copia invertida de
la primera. La segunda cadena reservará sólo la memoria que necesite.
· 230 Diseña una función que lea una cadena y construya otra que contenga un ejemplar de
cada carácter de la primera. Por ejemplo, si la primera cadena es "este ejemplo", la segunda
será "est jmplo". Ten en cuenta que la segunda cadena debe ocupar la menor cantidad de
memoria posible.
.............................................................................................
char * p = "cadena";
Pero, ¡ojo!, la cadena apuntada por p es, en ese caso, inmutable: si intentas asignar un
char a p[i], el programa puede abortar su ejecución. ¿Por qué? Porque los literales de
cadena ((residen)) en una zona de memoria especial (la denominada ((zona de texto))) que
está protegida contra escritura. Y hay una razón para ello: en esa zona reside, también,
el código de máquina correspondiente al programa. Que un programa modifique su propio
código de máquina es una pésima práctica (que era relativamente frecuente en los tiempos
en que predominaba la programación en ensamblador), hasta el punto de que su zona de
memoria se marca como de sólo lectura.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 231 Implementa una función que reciba una cadena y devuelva una copia invertida. (Ten
en cuenta que la talla de la cadena puede conocerse con strlen, ası́ que no es necesario que
suministres la talla explı́citamente ni que devuelvas la talla de la memoria solicitada con un
parámetro pasado por referencia.)
Escribe un programa que solicite varias palabras a un usuario y muestre el resultado de
invertir cada una de ellas.
.............................................................................................
4 int main(void)
5 {
6 float ** m = NULL ;
7 int filas, columnas;
8
12 /* reserva de memoria */
13 m = malloc(filas * sizeof (float *)) ;
14 for (i=0; i<filas; i++)
15 m[i] = malloc(columnas * sizeof (float)) ;
16
20 /* liberación de memoria */
21 for (i=0; i<filas; i++)
22 free(m[i]) ;
23 free(m) ;
24 m = NULL ;
25
26 return 0;
27 }
Empecemos por la declaración de la matriz (lı́nea 6). Es un puntero un poco extraño: se declara
como float ** m. Dos asteriscos, no uno. Eso es porque se trata de un puntero a un puntero de
enteros o, equivalentemente, un vector dinámico de vectores dinámicos de enteros.
Reserva de memoria
Sigamos. Las lı́neas 9 y 10 solicitan al usuario los valores de filas y columnas. En la lı́nea 13
encontramos una petición de memoria. Se solicita espacio para un número filas de punteros
a float. Supongamos que filas vale 4. Tras esa petición, tenemos la siguiente asignación de
memoria para m:
0
m
1
El vector m es un vector dinámico cuyos elementos son punteros (del tipo float *). De
momento, esos punteros no apuntan a ninguna zona de memoria reservada. De ello se encarga
la lı́nea 15. Dicha lı́nea está en un bucle, ası́ que se ejecuta para m[0], m[1], m[2], . . . El
efecto es proporcionar un bloque de memoria para cada celda de m. He aquı́ el efecto final:
0 1 2 3 4
0
m
0 1 2 3 4
1
0 1 2 3 4
2
0 1 2 3 4
3
...
free(m) ;
m = NULL ;
?
/* liberación de memoria incorrecta: qué es m[i] ahora que m vale NULL? */
for (i=0; i<filas; i++)
free(m[i]) ;
}
El parámetro indica que es de tipo ((puntero a punteros a enteros)). Una forma alternativa de
decir lo mismo es ésta:
Se lee más bien como ((vector de punteros a entero)). Pero ambas expresiones son sinónimas de
((vector de vectores a entero)). Uno se siente tentado de utilizar esta otra cabecera:
!
void muestra_matriz ( int m[][] ) // Mal!
Pero no funciona. Es incorrecta. C entiende que queremos pasar una matriz estática y que
hemos omitido el número de columnas.
Sigamos con la función:
1 #include <stdlib.h>
2
3 int main(void)
4 {
5 int ** m;
6 int filas, columnas;
7
8 filas = ...;
9 columnas = ...;
10
11 // Reserva de memoria.
12 m = malloc(filas * sizeof (int *));
13 m[0] = malloc(filas * columnas * sizeof (int));
14 for (i=1; i<filas; i++) m[i] = m[i-1] + columnas;
15
16 ...
17 // Liberación de memoria.
18 free(m[0]);
19 free(m);
20
21 return 0;
22 }
La clave está en la sentencia m[i] = m[i-1] + columnas: el contenido de m[i] pasa a ser
la dirección de memoria columnas celdas más a la derecha de la dirección m[i-1]. He aquı́
una representación gráfica de una matriz de 5 × 4:
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
m 0
Observa que necesitamos suministrar el número de filas y columnas explı́citamente para saber
qué rango de valores deben tomar i y j:
1 void muestra_matriz (int ** m, int filas, int columnas)
2 {
3 int i,j;
4
Supongamos ahora que nos piden una función que efectúe la liberación de la memoria de
una matriz:
1 void libera_matriz (int ** m, int filas, int columnas)
2 {
3 int i,j;
4
Ahora resulta innecesario el paso del número de columnas, pues no se usa en la función:
1 void libera_matriz (int ** m, int filas)
2 {
3 int i,j;
4
Falta un detalle que harı́a mejor a esta función: la asignación del valor NULL a m al final de
todo. Para ello tenemos que pasar una referencia a la matriz, y no la propia matriz:
1 void libera_matriz ( int *** m , int filas)
2 {
3 int i,j;
4
¡Qué horror! ¡Tres asteriscos en la declaración del parámetro m! C no es, precisamente, el colmo
de la elegancia.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 232 Diseña una función que reciba un número de filas y un número de columnas y devuelva
una matriz dinámica de enteros con filas×columnas elementos.
· 233 Diseña un procedimiento que reciba un puntero a una matriz dinámica (sin memo-
ria asignada), un número de filas y un número de columnas y devuelva, mediante el primer
parámetro, una matriz dinámica de enteros con filas×columnas elementos.
.............................................................................................
La gestión de matrices dinámicas considerando por separado sus tres variables (puntero a
memoria, número de filas y número de columnas) resulta poco elegante y da lugar a funciones
con parámetros de difı́cil lectura. En el siguiente apartado aprenderás a usar matrices dinámicas
que agrupan sus tres datos en un tipo registro definido por el usuario.
1 struct Matriz {
2 float ** m;
3 int filas, columnas;
4 };
Diseñemos ahora una función que ((cree)) una matriz dado el número de filas y el número de
columnas:
12 mat.filas = filas;
13 mat.columnas = columnas;
14 mat.m = malloc ( filas * sizeof (float *) );
15 for (i=0; i<filas; i++)
16 mat.m[i] = malloc ( columnas * sizeof (float) );
17 return mat;
18 }
También nos vendrá bien disponer de un procedimiento para liberar la memoria de una
matriz:
5 if (mat->m != NULL) {
6 for (i=0; i<filas; i++)
7 free(mat->m[i]);
8 free(mat->m);
9 }
10
11 mat->m = NULL;
12 mat->filas = 0;
13 mat->columnas = 0;
14 }
Para liberar la memoria de una matriz dinámica m, efectuaremos una llamada como ésta:
1 libera_matriz (&m);
Como hemos de leer dos matrices por teclado, diseñemos ahora una función capaz de leer
una matriz por teclado:
Observa que hemos llamado a crea_matriz tan pronto hemos sabido cuál era el número de
filas y columnas de la matriz.
Y ahora, implementemos un procedimiento que muestre por pantalla una matriz:
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 234 En muestra_matriz hemos pasado la matriz mat por valor. ¿Cuántos bytes se copiarán
en pila con cada llamada?
· 235 Diseña una nueva versión de muestra_matriz en la que mat se pase por referencia.
¿Cuántos bytes se copiarán en pila con cada llamada?
.............................................................................................
Podemos proceder ya mismo a implementar una función que multiplique dos matrices:
No todo par de matrices puede multiplicarse entre sı́. El número de columnas de la primera
ha de ser igual al número de filas de la segunda. Por eso devolvemos una matriz vacı́a (de 0 × 0)
cuando a.columnas es distinto de b.filas.
Ya podemos construir el programa principal:
1 #include <stdio.h>
2
3 ...definición de funciones...
4
5 int main(void)
6 {
7 struct Matriz a, b, c;
8
9 a = lee_matriz ();
10 b = lee_matriz ();
11 c = multiplica_matrices(a, b);
12 if (c.m == NULL)
13 printf ("Las matrices no son multiplicables\n");
14 else {
15 printf ("Resultado del producto:\n");
16 muestra_matriz (c);
17 }
18 libera_matriz (&a);
19 libera_matriz (&b);
20 libera_matriz (&c);
21
22 return 0;
23 }
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 236 Diseña una función que sume dos matrices.
· 237 Pasar estructuras por valor puede ser ineficiente, pues se debe obtener una copia en
pila de la estructura completa (en el caso de las matrices, cada variable de tipo struct Matriz
ocupa 12 bytes —un puntero y dos enteros—, cuando una referencia supone la copia de sólo 4
bytes). Modifica la función que multiplica dos matrices para que sus dos parámetros se pasen
por referencia.
· 238 Diseña una función que encuentre, si lo hay, un punto de silla en una matriz. Un punto
de silla es un elemento de la matriz que es o bien el máximo de su fila y el mı́nimo de su columna
a la vez, o bien el mı́nimo de su fila y el máximo de su columna a la vez. La función devolverá
cierto o falso dependiendo de si hay algún punto de silla. Si lo hay, el valor del primer punto de
silla encontrado se devolverá como valor de un parámetro pasado por referencia.
.............................................................................................
0
listapal a n u a l \0
1
d a d i v o s o \0
2
m a n o \0
3
t a c o \0
¿Ves? Es parecido a una matriz, pero no exactamente una matriz: cada palabra ocupa
tanta memoria como necesita, pero no más. Este programa solicita al usuario 4 palabras y las
almacena en una estructura como la dibujada:
5 #define PALS 4
6 #define MAXLON 80
7
8 int main(void)
9 {
10 char ** listapal ;
11 char linea[MAXLON+1];
12 int i;
13
27 /* Liberar memoria */
28 for (i=0; i<PALS; i++)
29 free(listapal [i]);
30 free(listapal );
31
32 return 0;
33 }
Este otro programa sólo usa memoria dinámica para las palabras, pero no para el vector de
palabras:
3 #include <string.h>
4
5 #define PALS 4
6 #define MAXLON 80
7
8 int main(void)
9 {
10 char * listapal [PALS];
11 char linea[MAXLON+1];
12 int i;
13
26 /* Liberar memoria */
27 for (i=0; i<PALS; i++)
28 free(listapal [i]);
29
30 return 0;
31 }
Fı́jate en cómo hemos definido listapal : como un vector estático de 4 punteros a caracteres
(char * listapal [PALS]).
Vamos a ilustrar el uso de este tipo de estructuras de datos con la escritura de una función
que reciba una cadena y devuelva un vector de palabras, es decir, vamos a implementar la
funcionalidad que ofrece Python con el método split. Empecemos por considerar la cabecera
de la función, a la que llamaremos extrae_palabras. Está claro que uno de los parámetros de
entrada es una cadena, o sea, un vector de caracteres:
??? extrae_palabras(char frase[], ???)
No hace falta suministrar la longitud de la cadena, pues ésta se puede calcular con la función
strlen. ¿Cómo representamos la información de salida? Una posibilidad es devolver un vector
de cadenas:
char ** extrae_palabras(char frase[], ???)
O sea, devolvemos un puntero (*) a una serie de datos de tipo char *, o sea, cadenas. Pero aún
falta algo: hemos de indicar explı́citamente cuántas palabras hemos encontrado:
char ** extrae_palabras(char frase[], int * numpals)
Hemos recurrido a un parámetro adicional para devolver el segundo valor. Dicho parámetro es
la dirección de un entero, pues vamos a modificar su valor. Ya podemos codificar el cuerpo de
la función. Empezaremos por contar las palabras, que serán series de caracteres separadas por
blancos (no entraremos en mayores complicaciones acerca de qué es una palabra).
1 char ** extrae_palabras(char frase[], int * numpals)
2 {
3 int i, lonfrase;
4
5 lonfrase = strlen(frase);
6 *numpals = 1;
7 for (i=0; i<lonfrase-1; i++)
8 if (frase[i] == ’ ’ && frase[i+1] != ’ ’) (*numpals)++;
9 if (frase[0] == ’ ’) (*numpals)--;
10
11 ...
12 }
saluda -n nombre
saluda.c
1 #include <stdio.h>
2 #include <string.h>
3
argv 0
s a l u d a \0
1
- n \0
2
n o m b r e \0
Ya podemos reservar memoria para el vector de cadenas, pero aún no para cada una de ellas:
1 char ** extrae_palabras(char frase[], int * numpals)
2 {
3 int i, lonfrase;
4 char **palabras;
5
6 lonfrase = strlen(frase);
7 *numpals = 1;
8 for (i=0; i<lonfrase-1; i++)
9 if (frase[i] == ’ ’ && frase[i+1] != ’ ’) (*numpals)++;
10 if (frase[0] == ’ ’) (*numpals)--;
11
14 ...
15 }
Ahora pasamos a reservar memoria para cada una de las palabras y, tan pronto hagamos cada
reserva, ((escribirla)) en su porción de memoria:
1 char ** extrae_palabras(char frase[], int * numpals)
2 {
3 int i, j, inicio_pal , longitud_pal , palabra_actual , lonfrase;
4 char **palabras;
5
6 lonfrase = strlen(frase);
7 *numpals = 1;
8 for (i=0; i<lonfrase-1; i++)
9 if (frase[i] == ’ ’ && frase[i+1] != ’ ’)
10 (*numpals)++;
11 if (frase[0] == ’ ’)
12 (*numpals)--;
13
16 palabra_actual = 0;
17 i = 0;
18 if (frase[0] == ’ ’)
19 while (frase[++i] == ’ ’ && i < lonfrase); // Saltamos blancos iniciales.
20
21 while (i<lonfrase) {
22 inicio_pal = i;
23 while (frase[++i] != ’ ’ && i < lonfrase); // Recorremos la palabra.
24
39 return palabras;
40 }
¡Buf! Complicado, ¿verdad? Veamos cómo se puede usar la función desde el programa principal:
palabras.c E palabras.c E
1 #include <stdio.h>
2 #include <stdlib.h>
3
.
.
.
43 }
44
45 int main(void)
46 {
47 char linea[MAXLON+1];
48 int numero_palabras, i;
49 char ** las_palabras;
50
57 return 0;
58 }
¿Ya? Aún no. Aunque este programa compila correctamente y se ejecuta sin problemas, hemos
de considerarlo incorrecto: hemos solicitado memoria conb malloc y no la hemos liberado con
free.
palabras 1.c palabras.c
1 #include <stdio.h>
2 #include <stdlib.h>
3
.
.
.
43 }
44
45 int main(void)
46 {
47 char linea[MAXLON+1];
48 int numero_palabras, i;
49 char ** las_palabras;
50
62 return 0;
63 }
Ahora sı́.
4 int main(void)
5 {
6 int *** a; // Tres asteriscos: vector de vectores de vectores de enteros.
7 int i, j, k;
8
9 // Reserva de memoria
10 a = malloc(3*sizeof (int **));
11 for (i=0; i<3; i++) {
12 a[i] = malloc(3*sizeof (int *));
13 for (j=0; j<3; j++)
14 a[i][j] = malloc(3*sizeof (int));
15 }
16
17 // Inicialización
18 for (i=0; i<3; i++)
19 for (j=0; j<3; j++)
20 for (k=0; k<3; k++)
21 a[i][j][k] = i+j+k;
22
23 // Impresión
24 for (i=0; i<3; i++)
25 for (j=0; j<3; j++)
26 for (k=0; k<3; k++)
27 printf ("%d %d %d: %d\n", i, j, k, a[i][j][k]);
28
29 // Liberación de memoria.
30 for (i=0; i<3; i++) {
31 for (j=0; j<3; j++)
32 free(a[i][j]);
33 free(a[i]);
34 }
35 free(a);
36 a = NULL;
37
38 return 0;
39 }
0 1 2
0 1 2
1 2 3
0 1 2
2 3 4
0 1 2
0 1 2
a
0 1 2 3
0 1 2
0 1 2
1
2 3 4
0 1 2
2 0 1 2
3 4 5
0 1 2
2 3 4
0 1 2
3 4 5
0 1 2
4 5 6
stdlib.h
1 ...
2 void * realloc(void * puntero, int bytes);
3 ...
1 #include <stdlib.h>
2
3 int main(void)
4 {
5 int * a;
6
15 return 0;
16 }
La función realloc recibe como primer argumento el puntero que indica la zona de memoria que
deseamos redimensionar y como segundo argumento, el número de bytes que deseamos asignar
ahora. La función devuelve el puntero a la nueva zona de memoria.
¿Qué hace exactamente realloc? Depende de si se pide más o menos memoria de la que ya
se tiene reservada:
Si se pide más memoria, intenta primero ampliar la zona de memoria asignada. Si las
posiciones de memoria que siguen a las que ocupa a en ese instante están libres, se las
asigna a a, sin más. En caso contrario, solicita una nueva zona de memoria, copia el
contenido de la vieja zona de memoria en la nueva, libera la vieja zona de memoria y nos
devuelve el puntero a la nueva.
Si se pide menos memoria, libera la que sobra en el bloque reservado. Un caso extremo
consiste en llamar a realloc con una petición de 0 bytes. En tal caso, la llamada a realloc
es equivalente a free.
Al igual que malloc, si realloc no puede atender una petición, devuelve un puntero a NULL. Una
cosa más: si se llama a realloc con el valor NULL como primer parámetro, realloc se comporta
como si se llamara a malloc.
Como puedes imaginar, un uso constante de realloc puede ser una fuente de ineficiencia.
Si tienes un vector que ocupa un 1 megabyte y usas realloc para que ocupe 1.1 megabyes, es
probable que provoques una copia de 1 megabyte de datos de la zona vieja a la nueva. Es más,
puede que ni siquiera tengas memoria suficiente para efectuar la nueva reserva, pues durante
un instante (mientras se efectúa la copia) estarás usando 2.1 megabytes.
Desarrollemos un ejemplo para ilustrar el concepto de reasignación o redimensionamiento de
memoria. Vamos a diseñar un módulo que permita crear diccionarios de palabras. Un diccionario
es un vector de cadenas. Cuando creamos el diccionario, no sabemos cuántas palabras albergará
ni qué longitud tiene cada una de las palabras. Tendremos que usar, pues, memoria dinámica.
Las palabras, una vez se introducen en el diccionario, no cambian de tamaño, ası́ que bastará
con usar malloc para gestionar sus reservas de memoria. Sin embargo, la talla de la lista de
palabras sı́ varı́a al añadir palabras, ası́ que deberemos gestionarla con malloc/realloc.
Empecemos por definir el tipo de datos para un diccionario.
1 struct Diccionario {
2 char ** palabra;
3 int palabras;
4 };
Aquı́ tienes un ejemplo de diccionario que contiene 4 palabras:
palabra
0
a n u a l \0
palabras
4 1
d a d i v o s o \0
2
m a n o \0
3
t a c o \0
Un diccionario vacı́o no tiene palabra alguna ni memoria reservada. Esta función crea un
diccionario:
Ya podemos desarrollar la función que inserta una palabra en el diccionario. Lo primero que
hará la función es comprobar si la palabra ya está en el diccionario. En tal caso, no hará nada:
5 if (d->palabra != NULL) {
6 for (i=0; i<d->palabras; i++)
7 free(d->palabra[i]);
8 free(d->palabra);
9 d->palabra = NULL;
10 d->palabras = 0;
11 }
12 }
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 239 Diseña una función que devuelva cierto (valor 1) o falso (valor 0) en función de si una
palabra pertenece o no a un diccionario.
· 240 Diseña una función que borre una palabra del diccionario.
· 241 Diseña una función que muestre por pantalla todas la palabras del diccionario que
empiezan por un prefijo dado (una cadena).
· 242 Diseña una función que muestre por pantalla todas la palabras del diccionario que
acaban con un sufijo dado (una cadena).
.............................................................................................
La función que determina si una palabra pertenece o no a un diccionario requiere tanto más
tiempo cuanto mayor es el número de palabras del diccionario. Es ası́ porque el diccionario está
desordenado y, por tanto, la única forma de estar seguros de que una palabra no está en el
diccionario es recorrer todas y cada una de las palabras (si, por contra, la palabra está en el
diccionario, no siempre es necesario recorrer el listado completo).
Podemos mejorar el comportamiento de la rutina de búsqueda si mantenemos el dicciona-
rio siempre ordenado. Para ello hemos de modificar la función de inserción de palabras en el
diccionario:
¡Buf! Las lı́neas 20–22 no hacen más que asignar a una palabra el contenido de otra (la
que ocupa la posición j recibe una copia del contenido de la que ocupa la posición j-1). ¿No
hay una forma mejor de hacer eso mismo? Sı́. Transcribimos nuevamente las últimas lı́neas del
programa, pero con una sola sentencia que sustituye a las lı́neas 20–22:
18 ...
19 for (j=d->palabras; j>i; i--)
20 d->palabra[j] = d->palabra[j-1] ;
21 /* Y copiamos en su celda la nueva palabra */
22 d->palabra[i] = malloc( (strlen(pal )+1) * sizeof (char) );
23 strcpy(d->palabra[i], pal );
24 d->palabras++;
25 }
No está mal, pero ¡no hemos pedido ni liberado memoria dinámica! ¡Ni siquiera hemos usado
strcpy, y eso que dijimos que habı́a que usar esa función para asignar una cadena a otra. ¿Cómo
es posible? Antes hemos de comentar qué significa una asignación como ésta:
1 d->palabra[j] = d->palabra[j-1];
Significa que d->palabra[j] apunta al mismo lugar al que apunta d->palabra[j-1]. ¿Por qué?
Porque un puntero no es más que una dirección de memoria y asignar a un puntero el valor de
otro hace que ambos contengan la misma dirección de memoria, es decir, que ambos apunten
al mismo lugar.
Veamos qué pasa estudiando un ejemplo. Imagina un diccionario en el que ya hemos insertado
las palabras ((anual)), ((dadivoso)), ((mano)) y ((taco)) y que vamos a insertar ahora la palabra
((feliz)). Partimos, pues, de esta situación:
palabra
0
a n u a l \0
palabras
4 1
d a d i v o s o \0
2
m a n o \0
3
t a c o \0
palabra
0
a n u a l \0
palabras
4 1
d a d i v o s o \0
2
m a n o \0
3
t a c o \0
4
palabra
0
a n u a l \0
palabras
4 1
d a d i v o s o \0
2
m a n o \0
3
t a c o \0
4
La ejecución de la asignación ha hecho que d->palabra[4] apunte al mismo lugar que d->palabra[3].
No hay problema alguno en que dos punteros apunten a un mismo bloque de memoria. En la
siguiente iteración pasamos a esta otra situación:
palabra
0
a n u a l \0
palabras
4 1
d a d i v o s o \0
2
m a n o \0
3
t a c o \0
4
Podemos reordenar gráficamente los elementos, para ver que, efectivamente, estamos haciendo
hueco para la nueva palabra:
palabra
0
a n u a l \0
palabras
4 1
d a d i v o s o \0
2
3
m a n o \0
4
t a c o \0
El bucle ha acabado. Ahora se pide memoria para el puntero d->palabra[i] (siendo i igual a
2). Se piden 6 bytes (((feliz)) tiene 5 caracteres más el terminador nulo):
palabra
0
a n u a l \0
palabras
4 1
d a d i v o s o \0
2
3
m a n o \0
4
t a c o \0
Podemos ahora implementar una función de búsqueda de palabras más eficiente. Una pri-
mera idea consiste en buscar desde el principio y parar cuando se encuentre la palabra buscada
o cuando se encuentre una palabra mayor (alfabéticamente) que la buscada. En este último
caso sabremos que la palabra no existe. Pero aún hay una forma más eficiente de saber si una
palabra está o no en una lista ordenada: mediante una búsqueda dicotómica.
5 izquierda = 0;
6 derecha = d.palabras;
7 while (izquierda < derecha) {
8 centro = (izquierda+derecha) / 2;
9 if (strcmp(pal , d.palabra[centro]) == 0)
10 return 1;
11 else if (strcmp(pal , d.palabra[centro]) < 0)
12 derecha = centro;
13 else
14 izquierda = centro+1;
15 }
16 return 0;
17 }
Podemos hacer una pequeña mejora para evitar el sobrecoste de llamar dos veces a la función
strcmp:
1 int buscar_en_diccionario(struct Diccionario d, char pal [])
2 {
3 int izquierda, centro, derecha, comparacion ;
4
5 izquierda = 0;
6 derecha = d.palabras;
7 while (izquierda < derecha) {
8 centro = (izquierda+derecha) / 2;
9 comparacion = strcmp(pal , d.palabra[centro]) ;
10 if ( comparacion == 0)
11 return 1;
12 else if ( comparacion < 0)
13 derecha = centro;
14 else
15 izquierda = centro+1;
16 }
17 return 0;
18 }
Juntemos todas las piezas y añadamos una función main que nos pida primero las palabras
del diccionario y, a continuación, nos pida palabras que buscar en él:
diccionario.c diccionario.c
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <string.h>
4
5 #define MAXLON 80
6
7 struct Diccionario {
8 char ** palabra;
9 int palabras;
10 };
11
15 d.palabra = NULL;
16 d.palabras = 0;
17 return d;
18 }
19
24 if (d->palabra != NULL) {
25 for (i=0; i<d->palabras; i++)
26 free(d->palabra[i]);
27 free(d->palabra);
28 d->palabra = NULL;
29 d->palabras = 0;
30 }
31 }
32
66 izquierda = 0;
67 derecha = d.palabras;
68 while (izquierda < derecha) {
69 centro = (izquierda+derecha) / 2;
70 comparacion = strcmp(pal , d.palabra[centro]);
71 if (comparacion == 0)
72 return 1;
73 else if (comparacion < 0)
74 derecha = centro;
75 else
76 izquierda = centro+1;
77 }
78 return 0;
79 }
80
81 int main(void)
82 {
83 struct Diccionario mi_diccionario;
84 int num_palabras;
85 char linea[MAXLON+1];
86
87 mi_diccionario = crea_diccionario();
88
?
89 printf (" Cuántas palabras tendrá el diccionario?: ");
90 gets(linea); sscanf (linea, "%d", &num_palabras);
91 while (mi_diccionario.palabras != num_palabras) {
92 printf ("Palabra %d: ", mi_diccionario.palabras+1);
93 gets(linea);
94 inserta_palabra_en_diccionario(&mi_diccionario, linea);
95 }
96
97 do {
?
98 printf (" Qué palabra busco? (pulsa retorno para acabar): ");
99 gets(linea);
100 if (strlen(linea) > 0) {
101 if (buscar_en_diccionario(mi_diccionario, linea))
102 printf ("Sı́ que está.\n");
103 else
104 printf ("No está.\n");
105 }
106 } while(strlen(linea) > 0);
107
108 libera_diccionario(&mi_diccionario);
109
110 return 0;
111 }
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 243 ¿Cuántas comparaciones se hacen en el peor de los casos en la búsqueda dicotómica de
una palabra cualquiera en un diccionario con 8 palabras? ¿Y en un diccionario con 16 palabras?
¿Y en uno con 32? ¿Y en uno con 1024? ¿Y en uno con 1048576? (Nota: el valor 1048576 es
igual a 220 .)
· 244 Al insertar una nueva palabra en un diccionario hemos de comprobar si existı́a previa-
mente y, si es una palabra nueva, averiguar en qué posición hay que insertarla. En la última
versión presentada, esa búsqueda se efectúa recorriendo el diccionario palabra a palabra. Mo-
difı́cala para que esa búsqueda sea dicotómica.
· 245 Diseña una función que funda dos diccionarios ordenados en uno sólo (también orde-
nado) que se devolverá como resultado. La fusión se hará de modo que las palabras que están
repetidas en ambos diccionarios aparezcan una sóla vez en el diccionario final.
.............................................................................................
Para que te hagas una idea del montaje, te mostramos la representación gráfica de las
estructuras de datos con las que representamos la agenda del ejemplo:
0 1 2 3 4 5 6 7 8 9 10
P e p e P é r e z \0
0 1 2 3 4 5 6 7 8 9 10
A n a G a r c ı́ a \0
0 1 2 3 4 5 6 7 8
J u a n G i l \0
0 1 2 3 4 5 6 7 8 9
M a r ı́ a P a z \0
0 1 2 3
persona nombre nombre nombre nombre
0 1 2 3 4 5 6 7 8 9 10
0
9 6 4 3 2 1 6 5 4 \0
0 1 2 3 4 5 6 7 8 9 10
1
9 6 4 9 8 7 6 5 4 \0
0 1 2 3 4 5 6 7 8 9 10
2
9 6 4 0 0 1 1 2 2 \0
0 1 2 3 4 5 6 7 8 9
0
9 6 1 1 1 1 1 1 \0
0 1 2 3 4 5 6 7 8 9
1
9 6 3 6 9 2 4 6 \0
4 /************************************************************************
5 * Entradas
6 ************************************************************************/
8
9 struct Entrada {
10 char * nombre; // Nombre de la persona.
11 char ** telefono; // Vector dinámico de números de teléfono.
12 int telefonos; // Número de elementos en el anterior vector.
13 };
14
21
22 e->telefono = NULL;
23 e->telefonos = 0;
24 }
25
49 free(e->nombre);
50 for (i=0; i<e->telefonos; i++)
51 free(e->telefono[i]);
52 free(e->telefono);
53
54 e->nombre = NULL;
55 e->telefono = NULL;
56 e->telefonos = 0;
57 }
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 246 Modifica anyadir_telefono_a_entrada para que compruebe si el teléfono ya habı́a sido
dado de alta. En tal caso, la función dejará intacta la lista de teléfonos de esa entrada.
.............................................................................................
Ya tenemos resuelta la gestión de entradas. Ocupémonos ahora del tipo agenda y de su
gestión.
1 /************************************************************************
2 * Agenda
3 ************************************************************************/
5
6 struct Agenda {
7 struct Entrada * persona; /* Vector de entradas */
8 int personas; /* Número de entradas en el vector */
9 };
10
15 a.persona = NULL;
16 a.personas = 0;
17 return a;
18 }
19
.
.
.
133
134 /************************************************************************
135 * Programa principal
136 ************************************************************************/
138
150 do {
151 printf ("Menú:\n");
152 printf ("1) Ver contenido completo de la agenda.\n");
153 printf ("2) Dar de alta una persona.\n");
154 printf ("3) A~nadir un teléfono.\n");
155 printf ("4) Buscar teléfonos de una persona.\n");
156 printf ("5) Salir.\n");
157 printf ("Opción: ");
158 gets(linea); sscanf (linea, "%d", &opcion);
159
160 switch(opcion) {
161
164 break;
165
199 libera_agenda(&miagenda);
200
201 return 0;
202 }
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 247 Diseña una función que permita eliminar una entrada de la agenda a partir del nombre
de una persona.
· 248 La agenda, tal y como la hemos implementado, está desordenada. Modifica el programa
para que esté siempre ordenada.
.............................................................................................
Cada vez que necesitamos escribir un nuevo dato en una celda adicional, comprobamos
si talla es menor o igual que capacidad . En tal caso, no hace falta redimensionar el vector,
basta con incrementar el valor de talla. Pero en caso contrario, nos curamos en salud y
redimensionamos pidiendo memoria para, pongamos, 10 celdas más (y, consecuentemente,
incrementamos el valor de capacidad en 10 unidades). De este modo reducimos el número
de llamadas a realloc a una décima parte. Incrementar un número fijo de celdas no es la
única estrategia posible. Otra aproximación consiste en duplicar la capacidad cada vez que
se precisa agrandar el vector. De este modo, el número de llamadas a realloc es proporcional
al logaritmo en base 2 del número de celdas del vector.
lista 3 8 2
1 struct Nodo {
2 float x ;
3 float y ;
4 struct Nodo * sig;
5 };
1 struct Punto {
2 float x;
3 float y;
4 };
5
6 struct Nodo {
7 struct Punto info;
8 struct Nodo * sig;
9 };
x x x
1.1 0.2 3.7
lista y y y
7.1 0.1 2.1
5 ...
No es más que un puntero a un elemento de tipo struct Nodo. Inicialmente, la lista está vacı́a.
Hemos de indicarlo explı́citamente ası́:
1 int main(void)
2 {
3 struct Nodo * lista = NULL ;
4
5 ...
Éste es el resultado:
info sig
lista
Ya tenemos el primer nodo de la lista, pero sus campos aún no tienen los valores que deben
tener finalmente. Lo hemos representado gráficamente dejando el campo info en blanco y sin
poner una flecha que salga del campo sig.
Por una parte, el campo info deberı́a contener el valor 8, y por otra, el campo sig deberı́a
apuntar a NULL:
1 int main(void)
2 {
3 struct Nodo * lista = NULL;
4
No debe sorprenderte el uso del operador -> en las asignaciones a campos del registro. La
variable lista es de tipo struct Nodo *, es decir, es un puntero, y el operador -> permite
acceder al campo de un registro apuntado por un puntero. He aquı́ el resultado:
info sig
lista 8
En primer lugar, hemos de crear un nuevo nodo al que deberá apuntar lista. El campo sig
del nuevo nodo, por su parte, deberı́a apuntar al nodo que contiene el valor 8. Empecemos por
la petición de un nuevo nodo que, ya que debe ser apuntado por lista, podemos pedir y rellenar
ası́:
1 int main(void)
2 {
3 struct Nodo * lista = NULL;
4
5 ...
6 lista = malloc( sizeof (struct Nodo) ) ;
7 lista->info = 3 ;
8 lista->sig = ??? ; // No sabemos cómo expresar esta asignación.
9 ...
¡Algo ha ido mal! ¿Cómo podemos asignar a lista->sig la dirección del siguiente nodo con valor
8? La situación en la que nos encontramos se puede representar ası́:
info sig
3
info sig
lista 8
¡No somos capaces de acceder al nodo que contiene el valor 8! Es lo que denominamos una pérdida
de referencia, un grave error en nuestro programa que nos imposibilita seguir construyendo la
lista. Si no podemos acceder a un bloque de memoria que hemos pedido con malloc, tampoco
podremos liberarlo luego con free. Cuando se produce una pérdida de referencia hay, pues,
una fuga de memoria: pedimos memoria al ordenador y no somos capaces de liberarla cuando
dejamos de necesitarla. Un programa con fugas de memoria corre el riesgo de consumir toda la
memoria disponible en el ordenador. Hemos de estar siempre atentos para evitar pérdidas de
referencia. Es uno de los mayores peligros del trabajo con memoria dinámica.
¿Cómo podemos evitar la pérdida de referencia? Muy fácil: con un puntero auxiliar.
1 int main(void)
2 {
3 struct Nodo * lista = NULL, * aux ;
4
5 ...
6 aux = lista ;
7 lista = malloc( sizeof (struct Nodo) );
8 lista->info = 3;
9 lista->sig = aux ;
10 ...
La declaración de la lı́nea 3 es curiosa. Cuando declaras dos o más punteros en una sola lı́nea,
has de poner el asterisco delante del identificador de cada puntero. En una lı́nea de declaración
que empieza por la palabra int puedes declarar punteros a enteros y enteros, según precedas
los respectivos identificadores con asterisco o no. Detengámonos un momento para considerar
el estado de la memoria justo después de ejecutarse la lı́nea 6, que reza ((aux = lista)):
aux
info sig
lista 8
El efecto de la lı́nea 6 es que tanto aux como lista apuntan al mismo registro. La asignación
de un puntero a otro hace que ambos apunten al mismo elemento. Recuerda que un puntero no
es más que una dirección de memoria y que copiar un puntero a otro hace que ambos contengan
la misma dirección de memoria, es decir, que ambos apunten al mismo lugar.
Sigamos con nuestra traza. Veamos cómo queda la memoria justo después de ejecutar la
lı́nea 7, que dice ((lista = malloc(sizeof (struct Nodo)))):
aux
La lı́nea 8, que dice ((lista->info = 3)), asigna al campo info del nuevo nodo (apuntado por
lista) el valor 3:
aux
La lista aún no está completa, pero observa que no hemos perdido la referencia al último
fragmento de la lista. El puntero aux la mantiene. Nos queda por ejecutar la lı́nea 9, que efectúa
la asignación ((lista->sig = aux )) y enlaza ası́ el campo sig del primer nodo con el segundo nodo,
el apuntado por aux . Tras ejecutarla tenemos:
aux
¡Perfecto! ¿Seguro? ¿Y qué hace aux apuntando aún a la lista? La verdad, nos da igual.
Lo importante es que los nodos que hay enlazados desde lista formen la lista que querı́amos
construir. No importa cómo quedan los punteros auxiliares: una vez han desempeñado su función
en la construcción de la lista, son supérfluos. Si te quedas más tranquilo, puedes añadir una
lı́nea con aux = NULL al final del programa para que aux no quede apuntando a un nodo de la
lista, pero, repetimos, es innecesario.
¿Qué hemos de hacer? Para empezar, pedir un nuevo nodo, sólo que esta vez no estará
apuntado por lista, sino por el que hasta ahora era el último nodo de la lista. De momento,
lo mantendremos apuntado por un puntero auxiliar. Después, accederemos de algún modo al
campo sig del último nodo de la lista (el que tiene valor 8) y haremos que apunte al nuevo
nodo. Finalmente, haremos que el nuevo nodo contenga el valor 2 y que tenga como siguiente
nodo a NULL. Intentémoslo:
1 int main(void)
2 {
3 struct Nodo * lista = NULL, * aux ;
4
5 ...
6 aux = malloc( sizeof (struct Nodo) ) ;
7 lista->sig->sig = aux ;
8 aux ->info = 2 ;
9 aux ->sig = NULL ;
10
11 return 0;
12 }
Veamos cómo queda la memoria paso a paso. Tras ejecutar la lı́nea 6 tenemos:
info sig
aux
O sea, la lista que ((cuelga)) de lista sigue igual, pero ahora aux apunta a un nuevo nodo.
Pasemos a estudiar la lı́nea 7, que parece complicada porque contiene varias aplicaciones del
operador ->. Esa lı́nea reza ası́: lista->sig->sig = aux . Vamos a ver qué significa leyéndola de
izquierda a derecha. Si lista es un puntero, y lista->sig es el campo sig del primer nodo, que es
otro puntero, entonces lista->sig->sig es el campo sig del segundo nodo, que es otro puntero.
Si a ese puntero le asignamos aux , el campo sig del segundo nodo apunta a donde apuntará
aux . Aquı́ tienes el resultado:
info sig
aux
Aún no hemos acabado. Una vez hayamos ejecutado las lı́neas 8 y 9, el trabajo estará
completo:
info sig
aux 2
Ahora queda más claro que, efectivamente, hemos conseguido el objetivo. Esta figura y la
anterior son absolutamente equivalentes.
Aún hay algo en nuestro programa poco elegante: la asignación ((lista->sig->sig = aux )) es
complicada de entender y da pie a un método de adición por el final muy poco ((extensible)).
¿Qué queremos decir con esto último? Que si ahora queremos añadir a la lista de 3 nodos
un cuarto nodo, tendremos que hacer ((lista->sig->sig->sig = aux )). Y si quisiéramos añadir
un quinto, ((lista->sig->sig->sig->sig = aux )) Imagina que la lista tiene 100 o 200 elementos.
¡Menuda complicación proceder ası́ para añadir por el final! ¿No podemos expresar la idea
((añadir por el final)) de un modo más elegante y general? Sı́. Podemos hacer lo siguiente:
1. buscar el último elemento con un bucle y mantenerlo referenciado con un puntero auxiliar,
digamos aux ;
aux
2. pedir un nodo nuevo y mantenerlo apuntado con otro puntero auxiliar, digamos nuevo;
aux
info sig
nuevo
3. escribir en el nodo apuntado por nuevo el nuevo dato y hacer que su campo sig apunte a
NULL;
aux
info sig
nuevo 2
4. hacer que el nodo apuntado por aux tenga como siguiente nodo al nodo apuntado por
nuevo.
aux
info sig
nuevo 2
nuevo
5 ...
6 aux = lista;
7 while (aux ->sig != NULL)
8 aux = aux ->sig;
9 nuevo = malloc( sizeof (struct Nodo) ) ;
10 nuevo->info = 2 ;
11 nuevo->sig = NULL ;
12 aux ->sig = nuevo ;
13
14 return 0;
15 }
La inicialización y el bucle de las lı́neas 6–8 buscan al último nodo de la lista y lo mantienen
apuntado con aux . El último nodo se distingue porque al llegar a él, aux ->sig es NULL, de ahı́ la
condición del bucle. No importa cuán larga sea la lista: tanto si tiene 1 elemento como si tiene
200, aux acaba apuntando al último de ellos.5 Si partimos de una lista con dos elementos, éste
es el resultado de ejecutar el bucle:
aux
nuevo
info sig
nuevo 2
info sig
nuevo 2
5 ...
6 for (aux = lista; aux ->sig != NULL; aux = aux ->sig) ;
7 nuevo = malloc( sizeof (struct Nodo) );
8 nuevo->info = 2;
9 nuevo->sig = NULL;
10 aux ->sig = nuevo;
11
12 return 0;
13 }
Observa que el punto y coma que aparece al final del bucle for hace que no tenga sentencia
alguna en su bloque:
1 for (aux = lista; aux ->sig != NULL; aux = aux ->sig) ;
5 Aunque falla en un caso: si la lista está inicialmente vacı́a. Estudiaremos este problema y su solución más
adelante.
El bucle se limita a ((desplazar)) el puntero aux hasta que apunte al último elemento de la lista.
Esta expresión del bucle que busca el elemento final es más propia de la programación C, más
idiomática.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 249 Hemos diseñado un método (que mejoraremos en el siguiente apartado) que permite
insertar elementos por el final de una lista y hemos necesitado un bucle. ¿Hará falta un bucle
para insertar un elemento por delante en una lista cualquiera? ¿Cómo harı́as para convertir la
última lista en esta otra?:
info sig info sig info sig info sig
lista 1 3 8 2
.............................................................................................
Como lo que deseamos es que lista pase a apuntar al segundo elemento de la lista, podrı́amos
diseñar una aproximación directa modificando el valor de lista:
1 int main(void)
2 {
3 struct Nodo * lista = NULL, * aux , * nuevo;
4
5 ...
!
6 lista = lista->sig; // Mal! Se pierde la referencia a la cabeza original de la lista.
7
8 return 0;
9 }
Efectivamente, hemos conseguido que la lista apuntada por lista sea lo que pretendı́amos,
pero hemos perdido la referencia a un nodo (el que hasta ahora era el primero) y ya no podemos
liberarlo. Hemos provocado una fuga de memoria.
Para liberar un bloque de memoria hemos de llamar a free con el puntero que apunta a la
dirección en la que empieza el bloque. Nuestro bloque está apuntado por lista, ası́ que podrı́amos
pensar que la solución es trivial y que bastarı́a con llamar a free antes de modificar lista:
1 int main(void)
2 {
3 struct Nodo * lista = NULL, * aux , * nuevo;
4
5 ...
6 free(lista);
!
7 lista = lista->sig ; // Mal! lista no apunta a una zona de memoria válida.
8
9 return 0;
10 }
Pero, claro, no iba a resultar tan sencillo. ¡La lı́nea 7, que dice ((lista = lista->sig)), no puede
ejecutarse! Tan pronto hemos ejecutado la lı́nea 6, tenemos otra fuga de memoria:
info sig info sig
lista 8 2
O sea, hemos liberado correctamente el primer nodo, pero ahora hemos perdido la referencia
al resto de nodos y el valor de lista->sig está indefinido. ¿Cómo podemos arreglar esto? Si no
liberamos memoria, hay una fuga, y si la liberamos perdemos la referencia al resto de la lista.
La solución es sencilla: guardamos una referencia al resto de la lista con un puntero auxiliar
cuando aún estamos a tiempo.
1 int main(void)
2 {
3 struct Nodo * lista = NULL, * aux , * nuevo;
4
5 ...
6 aux = lista->sig ;
7 free(lista);
8 lista = aux ;
9
10 return 0;
11 }
Ahora sı́. Veamos paso a paso qué hacen las últimas tres lı́neas del programa. La asignación
aux = lista->sig introduce una referencia al segundo nodo:
aux
No hay problema. Seguimos sabiendo dónde está el resto de la lista: ((cuelga)) de aux . Ası́
pues, podemos llegar al resultado deseado con la asignación lista = aux :
aux
¿Vas viendo ya el tipo de problemas al que nos enfrentamos con la gestión de listas? Los
siguientes apartados te presentan funciones capaces de inicializar listas, de insertar, borrar
y encontrar elementos, de mantener listas ordenadas, etc. Cada apartado te presentará una
variante de las listas enlazadas con diferentes prestaciones que permiten elegir soluciones de
compromiso entre velocidad de ciertas operaciones, consumo de memoria y complicación de la
implementación.
Como ya dijimos, este tipo de nodo sólo alberga un número entero. Si necesitásemos una lista
de float deberı́amos cambiar el tipo del valor del campo info. Y si quisiésemos una lista de
((personas)), podrı́amos añadir varios campos a struct Nodo (uno para el nombre, otro para la
edad, etc.) o declarar info como de un tipo struct Persona definido previamente por nosotros.
Una lista es un puntero a un struct Nodo, pero cuesta poco definir un nuevo tipo para
referirnos con mayor brevedad al tipo ((lista)):
lista.h
...
typedef struct Nodo * TipoLista ;
Ahora, podemos declarar una lista como struct Nodo * o como TipoLista, indistintamente.
Por claridad, nos referiremos al tipo de una lista con TipoLista y al de un puntero a un nodo
cualquiera con struct Nodo *, pero no olvides que ambos tipos son equivalentes.
Como struct Nodo y TipoNodo son sinónimos, pronto se intenta definir la estructura
ası́:
lista.c
1 #include <stdlib.h>
2 #include "lista.h"
3
4 TipoLista lista_vacia(void)
5 {
6 return NULL;
7 }
4 int main(void)
5 {
6 TipoLista lista;
7
8 lista = lista_vacia();
9
10 return 0;
11 }
Ciertamente podrı́amos haber hecho lista = NULL, sin más, pero queda más elegante propor-
cionar funciones para cada una de las operaciones básicas que ofrece una lista, y crear una lista
vacı́a es una operación básica.
miprograma.c
1 #include "lista.h"
2
3 int main(void)
4 {
5 TipoLista lista;
6
7 lista = lista_vacia();
8 lista = inserta_por_cabeza(lista, 2);
9 lista = inserta_por_cabeza(lista, 8);
10 lista = inserta_por_cabeza(lista, 3);
11 ...
12 return 0;
13 }
miprograma.c
1 #include "lista.h"
2
3 int main(void)
4 {
5 TipoLista lista;
6
7 lista = inserta_por_cabeza(inserta_por_cabeza(inserta_por_cabeza(lista_vacia(),2),8),3);
8 ...
9 return 0;
10 }
lista.c
1 TipoLista inserta_por_cabeza(TipoLista lista, int valor )
2 {
3 struct Nodo * nuevo = malloc(sizeof (struct Nodo));
4
5 nuevo->info = valor ;
6 ...
7 }
Ahora hemos de pensar un poco. Si lista va a tener como primer elemento a nuevo, ¿podemos
enlazar directamente lista con nuevo?
lista.c
1 TipoLista inserta_por_cabeza(TipoLista lista, int valor )@mal
2 {
3 struct Nodo * nuevo = malloc(sizeof (struct Nodo));
4
5 nuevo->info = valor ;
6 lista = nuevo ;
7 ...
8 }
La respuesta es no. Aún no podemos. Si lo hacemos, no hay forma de enlazar nuevo->sig con
lo que era la lista anteriormente. Hemos perdido la referencia a la lista original. Veámoslo con
un ejemplo. Imagina una lista como ésta:
info sig info sig
lista 8 2
La ejecución de la función (incompleta) con valor igual a 3 nos lleva a esta otra situación:
info sig
nuevo 3
Hemos perdido la referencia a la ((vieja)) lista. Una solución sencilla consiste en, antes de modi-
ficar lista, asignar a nuevo->sig el valor de lista:
1 TipoLista inserta_por_cabeza(TipoLista lista, int valor )
2 {
3 struct Nodo * nuevo = malloc(sizeof (struct Nodo));
4
5 nuevo->info = valor ;
6 nuevo->sig = lista ;
7 lista = nuevo;
8 return lista;
9 }
Las lı́neas 5 y 6 modifican los campos del nodo apuntado por nuevo:
info sig
nuevo 3
Finalmente, la lı́nea 7 hace que lista apunte a donde nuevo apunta. El resultado final es
éste:
info sig
nuevo 3
Sólo resta redisponer gráficamente la lista para que no quepa duda de la corrección de la
solución:
nuevo
Hemos visto, pues, que el método es correcto cuando la lista no está vacı́a. ¿Lo será también
si suministramos una lista vacı́a? La lista vacı́a es un caso especial para el que siempre deberemos
considerar la validez de nuestros métodos.
Hagamos una comprobación gráfica. Si partimos de esta lista:
lista
y ejecutamos la función (con valor igual a 10, por ejemplo), pasaremos momentáneamente por
esta situación:
info sig
nuevo 10
lista
info sig
lista 10
Ha funcionado correctamente. No tendremos tanta suerte con todas las funciones que vamos
a diseñar.
La implementación se basa en recorrer toda la lista con un bucle que desplace un puntero
hasta llegar a NULL. Con cada salto de nodo a nodo, incrementaremos un contador cuyo valor
final será devuelto por la función:
lista.c
1 int longitud_lista(TipoLista lista)
2 {
3 struct Nodo * aux ;
4 int contador = 0;
5
la variable contador empieza valiendo 0 y el bucle inicializa aux haciendo que apunte al primer
elemento:
aux
En la primera iteración, contador se incrementa en una unidad y aux pasa a apuntar al segundo
nodo:
aux
Acto seguido, en la segunda iteración, contador pasa a valer 2 y aux pasa a apuntar al tercer
nodo:
aux
aux
Ahı́ acaba el bucle. El valor devuelto por la función es 3, el número de nodos de la lista.
Observa que longitud_lista tarda más cuanto mayor es la lista. Una lista con n nodos obliga
a efectuar n iteraciones del bucle for. Algo similar (aunque sin manejar listas enlazadas) nos
ocurrı́a con strlen, la función que calcula la longitud de una cadena.
La forma de usar esta función desde el programa principal es sencilla:
miprograma.c
1 #include <stdio.h>
2 #include "lista.h"
3
4 int main(void)
5 {
6 TipoLista lista;
7 ...
8 printf ("Longitud: %d\n", longitud_lista(lista) );
9
10 return 0;
11 }
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 250 ¿Funcionará correctamente longitud_lista cuando le pasamos una lista vacı́a?
· 251 Diseña una función que reciba una lista de enteros con enlace simple y devuelva el valor
de su elemento máximo. Si la lista está vacı́a, se devolverá el valor 0.
· 252 Diseña una función que reciba una lista de enteros con enlace simple y devuelva su
media. Si la lista está vacı́a, se devolverá el valor 0.
.............................................................................................
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 253 Diseña un procedimiento que muestre el contenido de una lista al estilo Python. Por
ejemplo, la lista de la última figura se mostrará como [3, 8, 2]. Fı́jate en que la coma sólo
aparece separando a los diferentes valores, no después de todos los números.
· 254 Diseña un procedimiento que muestre el contenido de una lista como se indica en el
siguiente ejemplo. La lista formada por los valores 3, 8 y 2 se representará ası́:
->[3]->[8]->[2]->|
(La barra vertical representa a NULL.)
.............................................................................................
lista.h
...
extern TipoLista inserta_por_cola(TipoLista lista, int valor );
Nuestra función se dividirá en dos etapas: una primera que localice al último elemento de
la lista, y otra que cree el nuevo nodo y lo una a la lista.
Aquı́ tienes la primera etapa:
E lista.c E
1 TipoLista inserta_por_cola(TipoLista lista, int valor )
2 {
3 struct Nodo * aux ;
4
Analicemos paso a paso el bucle con un ejemplo. Imagina que la lista que nos suministran
en lista ya tiene tres nodos:
info sig info sig info sig
lista 3 8 2
La primera iteración del bucle hace que aux apunte al primer elemento de la lista:
aux
Habrá una nueva iteración si aux ->sig es distinto de NULL, es decir, si el nodo apuntado por
aux no es el último de la lista. Es nuestro caso, ası́ que iteramos haciendo aux = aux ->sig, o
sea, pasamos a esta nueva situación:
aux
¿Sigue siendo cierto que aux ->sig es distinto de NULL? Sı́. Avanzamos aux un nodo más a la
derecha:
aux
¿Y ahora? ¿Es cierto que aux ->sig es distinto de NULL? No, es igual a NULL. Ya hemos
llegado al último nodo de la lista. Fı́jate en que hemos parado un paso antes que cuando
contábamos el número de nodos de una lista; entonces la condición de iteración del bucle era
otra: ((aux != NULL)).
Podemos proceder con la segunda fase de la inserción: pedir un nuevo nodo y enlazarlo desde
el actual último nodo. Nos vendrá bien un nuevo puntero auxiliar:
E lista.c E
1 TipoLista inserta_por_cola(TipoLista lista, int valor )
2 {
3 struct Nodo * aux , * nuevo ;
4
El efecto de la ejecución de las nuevas lı́neas, suponiendo que el valor es 10, es éste:
info sig
nuevo 10
aux
Está claro que ha funcionado correctamente, ¿no? Tal vez resulte de ayuda ver la misma es-
tructura reordenada ası́:
aux nuevo
Bien, entonces, ¿por qué hemos marcado la función como incorrecta? Veamos qué ocurre si
la lista que nos proporcionan está vacı́a. Si la lista está vacı́a, lista vale NULL. En la primera
iteración del bucle for asignaremos a aux el valor de lista, es decir, NULL. Para ver si pasamos a
efectuar la primera iteración, hemos de comprobar antes si aux ->sig es distinto de NULL. ¡Pero
es un error preguntar por el valor de aux ->sig cuando aux es NULL! Un puntero a NULL no
apunta a nodo alguno, ası́ que no podemos preguntar por el valor del campo sig de un nodo
que no existe. ¡Ojo con este tipo de errores!: los accesos a memoria que no nos ((pertenece))
no son detectables por el compilador. Se manifiestan en tiempo de ejecución y, normalmente,
con consecuencias desastrosas6 , especialmente al efectuar escrituras de información. ¿Cómo
podemos corregir la función? Tratando a la lista vacı́a como un caso especial:
lista.c
1 TipoLista inserta_por_cola(TipoLista lista, int valor )
2 {
3 struct Nodo * aux , * nuevo;
4
5 if (lista == NULL) {
6 lista = malloc(sizeof (struct Nodo));
7 lista->info = valor ;
8 lista->sig = NULL;
9 }
10 else {
11 for (aux = lista; aux ->sig != NULL; aux = aux ->sig) ;
12 nuevo = malloc(sizeof (struct Nodo));
13 nuevo->info = valor ;
14 nuevo->sig = NULL;
15 aux ->sig = nuevo;
16 }
17 return lista;
18 }
Como puedes ver, el tratamiento de la lista vacı́a es muy sencillo, pero especial. Ya te lo
advertimos antes: comprueba siempre si tu función se comporta adecuadamente en situaciones
extremas. La lista vacı́a es un caso para el que siempre deberı́as comprobar la validez de tu
aproximación.
La función puede retocarse factorizando acciones comunes a los dos bloques del if -else:
6 En Linux, por ejemplo, obtendrás un error (tı́picamente ((Segmentation fault))) y se abortará inmediatamente
la ejecución del programa. En Microsoft Windows es frecuente que el ordenador ((se cuelgue)).
lista.c
1 TipoLista inserta_por_cola(TipoLista lista, int valor )
2 {
3 struct Nodo * aux , * nuevo;
4
Mejor ası́.
Tampoco podemos empezar haciendo free(lista) para liberar el primer nodo, pues entonces
perderı́amos la referencia al resto de nodos. La memoria quedarı́a ası́:
info sig info sig info sig
lista 3 8 2
5 aux = lista->sig;
6 free(lista);
7 lista = aux ;
8 return lista;
9 }
Ahora sı́, ¿no? No. Falla en el caso de que lista valga NULL, es decir, cuando nos pasan una
lista vacı́a. La asignación aux = lista->sig es errónea si lista es NULL. Pero la solución es muy
sencilla en este caso: si nos piden borrar el nodo de cabeza de una lista vacı́a, ¿qué hemos de
hacer? ¡Absolutamente nada!:
lista.c
1 TipoLista borra_cabeza(TipoLista lista)
2 {
5 if (lista != NULL) {
6 aux = lista->sig;
7 free(lista);
8 lista = aux ;
9 }
10 return lista;
11 }
Tenlo siempre presente: si usas la expresión aux ->sig para cualquier puntero aux , has de
estar completamente seguro de que aux no es NULL.
lista.h
...
extern TipoLista borra_cola(TipoLista lista);
2. y hacer que el hasta ahora penúltimo nodo tenga como valor de su campo sig a NULL.
E lista.c E
1 TipoLista borra_cola(TipoLista lista)
2 {
3 struct Nodo * aux ;
4
¡Alto! Este mismo bucle ya nos dió problemas cuando tratamos de insertar por la cola: no
funciona correctamente con listas vacı́as. De todos modos, el problema tiene fácil solución: no
tiene sentido borrar nada de una lista vacı́a.
E lista.c E
1 TipoLista borra_cola(TipoLista lista)
2 {
3 struct Nodo * aux ;
4
5 if (lista != NULL) {
6 for (aux = lista; aux ->sig != NULL; aux = aux ->sig) ;
7 ...
8 }
9 return lista;
10 }
Ahora el bucle solo se ejecuta con listas no vacı́as. Si partimos de esta lista:
aux
aux
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 255 ¿Seguro que el bucle de borra_cola funciona correctamente siempre? Piensa si hace lo
correcto cuando se le pasa una lista formada por un solo elemento.
.............................................................................................
Si hemos localizado ya el último nodo de la lista, hemos de liberar su memoria:
E lista.c E
1 TipoLista borra_cola(TipoLista lista)
2 {
3 struct Nodo * aux ;
4
5 if (lista != NULL) {
6 for (aux = lista; aux ->sig != NULL; aux = aux ->sig) ;
7 free(aux );
8 ...
9 }
10 }
Fı́jate: sólo nos falta conseguir que el nuevo último nodo (el de valor igual a 8) tenga como
valor del campo sig a NULL. Problema: ¿y cómo sabemos cuál es el último nodo? No se puede
saber. Ni siquiera utilizando un nuevo bucle de búsqueda del último nodo, ya que dicho bucle
se basaba en que el último nodo es reconocible porque tiene a NULL como valor de sig, y ahora
el último no apunta con sig a NULL.
El ((truco)) consiste en usar otro puntero auxiliar y modificar el bucle de búsqueda del último
para haga que el nuevo puntero auxiliar vaya siempre ((un paso por detrás)) de aux . Observa:
E lista.c E
1 TipoLista borra_cola(TipoLista lista)
2 {
3 struct Nodo * aux , * atras ;
4
5 if (lista != NULL) {
6 for ( atras = NULL, aux = lista; aux ->sig != NULL; atras = aux , aux = aux ->sig) ;
7 free(aux );
8 ...
9 }
10 }
Fı́jate en el nuevo aspecto del bucle for. Utilizamos una construcción sintáctica que aún no
conoces, ası́ que nos detendremos brevemente para explicarla. Los bucles for permiten trabajar
con más de una inicialización y con más de una acción de paso a la siguiente iteración. Este
bucle, por ejemplo, trabaja con dos variables enteras, una que toma valores crecientes y otra
que toma valores decrecientes:
1 for ( i=0 , j=10 ; i<3; i++ , j-- )
2 printf ("%d %d\n", i, j);
¡Ojo! Es un único bucle, no son dos bucles anidados. ¡No te confundas! Las diferentes inicia-
lizaciones y pasos de iteración se separan con comas. Al ejecutarlo, por pantalla aparecerá
esto:
0 10
1 9
2 8
Sigamos con el problema que nos ocupa. Veamos, paso a paso, qué hace ahora el bucle. En
la primera iteración tenemos:
atras aux
Y en la segunda iteración:
atras aux
Y en la tercera:
atras aux
¿Ves? No importa cuán larga sea la lista; el puntero atras siempre va un paso por detrás del
puntero aux . En nuestro ejemplo ya hemos llegado al final de la lista, ası́ que ahora podemos
liberar el nodo apuntado por aux :
atras aux
Ahora podemos continuar: ya hemos borrado el último nodo, pero esta vez sı́ que sabemos cuál
es el nuevo último nodo.
E lista.c E
1 TipoLista borra_cola(TipoLista lista)
2 {
3 struct Nodo * aux , * atras ;
4
5 if (lista != NULL) {
6 for (atras = NULL, aux = lista; aux ->sig != NULL; atras = aux , aux = aux ->sig) ;
7 free(aux );
8 atras->sig = NULL;
9 ...
10 }
11 }
Aún no hemos acabado. La función borra_cola trabaja correctamente con la lista vacı́a,
pues no hace nada en ese caso (no hay nada que borrar), pero, ¿funciona correctamente cuando
suministramos una lista con un único elemento? Hagamos una traza.
Tras ejecutar el bucle que busca a los elementos último y penúltimo, los punteros atras y
aux quedan ası́:
atras aux
info sig
lista 3
lista
Y, finalmente, hacemos que atras->sig sea igual NULL. Pero, ¡eso es imposible! El puntero
atras apunta a NULL, y hemos dicho ya que NULL no es un nodo y, por tanto, no tiene campo
alguno.
Tratemos este caso como un caso especial. En primer lugar, ¿cómo podemos detectarlo?
Viendo si atras vale NULL. ¿Y qué hemos de hacer entonces? Hemos de hacer que lista pase a
valer NULL, sin más.
lista.c
1 TipoLista borra_cola(TipoLista lista)
2 {
3 struct Nodo * aux , * atras ;
4
5 if (lista != NULL) {
6 for (atras = NULL, aux = lista; aux ->sig != NULL; atras = aux , aux = aux ->sig) ;
7 free(aux );
8 if (atras == NULL)
9 lista = NULL;
10 else
11 atras->sig = NULL;
12 }
13 return lista;
14 }
lista
Hemos aprendido una lección: otro caso especial que conviene estudiar explı́citamente es el
de la lista compuesta por un solo elemento.
Insistimos en que debes seguir una sencilla regla en el diseño de funciones con punteros: si
accedes a un campo de un puntero ptr , por ejemplo, ptr ->sig o ptr ->info, pregúntate siempre si
cabe alguna posibilidad de que ptr sea NULL; si es ası́, tienes un problema que debes solucionar.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 256 ¿Funcionan correctamente las funciones que hemos definido antes (cálculo de la lon-
gitud, inserción por cabeza y por cola y borrado de cabeza) cuando se suministra una lista
compuesta por un único elemento?
.............................................................................................
lista.c
1 int pertenece(TipoLista lista, int valor )
2 {
3 struct Nodo * aux ;
4
lista.h
...
extern TipoLista borra_primera_ocurrencia(TipoLista lista, int valor );
E lista.c E
1 TipoLista borra_primera_ocurrencia(TipoLista lista, int valor )
2 {
3 struct Nodo * aux ;
4
11 }
Veamos con un ejemplo en qué situación estamos cuando llegamos a la lı́nea marcada con
puntos suspensivos. En esta lista hemos buscado el valor 8, ası́ que podemos representar la
memoria ası́:
aux
Nuestro objetivo ahora es, por una parte, efectuar el siguiente ((empalme)) entre nodos:
aux
aux
Problema: ¿cómo hacemos el ((empalme))? Necesitamos conocer cuál es el nodo que precede
al que apunta aux . Eso sabemos hacerlo con ayuda de un puntero auxiliar que vaya un paso
por detrás de aux :
E lista.c E
1 TipoLista borra_primera_ocurrencia(TipoLista lista, int valor )
2 {
3 struct Nodo * aux , * atras ;
4
5 for ( atras = NULL , aux =lista; aux != NULL; atras = aux , aux = aux ->sig)
6 if (aux ->info == valor ) {
7 atras->sig = aux ->sig;
8 ...
9 }
10 return lista;
11 }
El puntero atras empieza apuntando a NULL y siempre va un paso por detrás de aux .
atras aux
Es decir, cuando aux apunta a un nodo, atras apunta al anterior. La primera iteración
cambia el valor de los punteros y los deja en este estado:
atras aux
¿Es correcta la función? Hay una fuente de posibles problemas. Estamos asignando algo a
atras->sig. ¿Cabe alguna posibilidad de que atras sea NULL? Sı́. El puntero atras es NULL cuando
el elemento encontrado ocupa la primera posición. Fı́jate en este ejemplo en el que queremos
borrar el elemento de valor 3:
atras aux
lista.c
1 TipoLista borra_primera_ocurrencia(TipoLista lista, int valor )
2 {
3 struct Nodo * aux , * atras ;
4
5 for (atras = NULL, aux =lista; aux != NULL; atras = aux , aux = aux ->sig)
6 if (aux ->info == valor ) {
7 if (atras == NULL)
Ahora podemos borrar el elemento apuntado por aux con tranquilidad y devolver la lista mo-
dificada:
lista.c
1 TipoLista borra_primera_ocurrencia(TipoLista lista, int valor )
2 {
3 struct Nodo * aux , * atras ;
4
5 for (atras = NULL, aux =lista; aux != NULL; atras = aux , aux = aux ->sig)
6 if (aux ->info == valor ) {
7 if (atras == NULL)
8 lista = aux ->sig;
9 else
10 atras->sig = aux ->sig;
11 free(aux );
12 return lista;
13 }
14 return lista;
15 }
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 258 ¿Funciona borra_primera_ocurrencia cuando ningún nodo de la lista contiene el valor
buscado?
· 259 ¿Funciona correctamente en los siguientes casos?
lista vacı́a;
y nos piden eliminar todos los nodos cuyo campo info vale 8.
Nuestro problema es localizar el primer 8 y borrarlo dejando los dos punteros auxiliares en
un estado tal que podamos seguir iterando para encontrar y borrar el siguiente 8 en la lista (y
ası́ con todos los que haya). Ya sabemos cómo localizar el primer 8. Si usamos un bucle con dos
punteros (aux y atras), llegamos a esta situación:
atras aux
info sig info sig info sig info sig info sig
lista 3 8 2 8 1
Si eliminamos el nodo apuntado por aux , nos interesa que aux pase a apuntar al siguiente,
pero que atras quede apuntando al mismo nodo al que apunta ahora (siempre ha de ir un paso
por detrás de aux ):
atras aux
Bueno. No resultará tan sencillo. Deberemos tener en cuenta qué ocurre en una situación
especial: el borrado del primer elemento de una lista. Aquı́ tienes una solución:
lista.c
1 TipoLista borra_valor (TipoLista lista, int valor )
2 {
3 struct Nodo * aux , * atras ;
4
5 atras = NULL;
6 aux = lista;
7 while (aux != NULL) {
8 if (aux ->info == valor ) {
9 if (atras == NULL)
10 lista = aux ->sig;
11 else
12 atras->sig = aux ->sig;
13 free(aux );
14 if (atras == NULL)
15 aux = lista;
16 else
17 aux = atras->sig;
18 }
19 else {
20 atras = aux ;
21 aux = aux ->sig;
22 }
23 }
24 return lista;
25 }
Hemos optado por un bucle while en lugar de un bucle for porque necesitamos un mayor
control de los punteros auxiliares (con el for, en cada iteración avanzamos ambos punteros y
no siempre queremos que avancen).
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 260 ¿Funciona borra_valor con listas vacı́as? ¿Y con listas de un sólo elemento? ¿Y con una
lista en la que todos los elementos coinciden en valor con el entero que buscamos? Si falla en
alguno de estos casos, corrige la función.
.............................................................................................
while y for
Hemos dicho que el bucle for no resulta conveniente cuando queremos tener un gran control
sobre los punteros auxiliares. No es cierto. El bucle for de C permite emular a cualquier
bucle while. Aquı́ tienes una versión de borra_valor (eliminación de todos los nodos con
un valor dado) que usa un bucle for:
Observa que en el bucle for hemos dejado en blanco la zona que indica cómo modificar los
punteros aux y atras. Puede hacerse. De hecho, puedes dejar en blanco cualquiera de los
componentes de un bucle for. Una alternativa a while (1), por ejemplo, es for (;;).
9 for (i=0, atras=NULL, aux =lista; i < pos && aux != NULL; i++, atras = aux , aux = aux ->sig) ;
10 nuevo->sig = aux ;
11 if (atras == NULL)
12 lista = nuevo;
13 else
14 atras->sig = nuevo;
15 return lista;
16 }
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 261 Modifica la función para que, si nos pasan un número de posición mayor que el número
de elementos de la lista, no se realice inserción alguna.
.............................................................................................
lista.c
1 TipoLista inserta_en_orden(TipoLista lista, int valor );
2 {
3 struct Nodo * aux , * atras, * nuevo;
4
8 for (atras = NULL, aux = lista; aux != NULL; atras = aux , aux = aux ->sig)
9 if (valor <= aux ->info) {
10 /* Aquı́ insertamos el nodo entre atras y aux. */
11 nuevo->sig = aux ;
12 if (atras == NULL)
13 lista = nuevo;
14 else
15 atras->sig = nuevo;
16 /* Y como ya está insertado, acabamos. */
17 return lista;
18 }
19 /* Si llegamos aquı́, es que nuevo va al final de la lista. */
20 nuevo->sig = NULL;
21 if (atras == NULL)
22 lista = nuevo;
23 else
24 atras->sig = nuevo;
25 return lista;
26 }
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 262 Haz una traza de la inserción del valor 7 con inserta_en_orden en cada una de estas
listas:
a)
b)
c)
d)
lista
e)
info sig
lista 1
f)
info sig
lista 10
· 263 Diseña una función de inserción ordenada en lista que inserte un nuevo nodo si y sólo
si no habı́a ningún otro con el mismo valor.
· 264 Determinar la pertenencia de un valor a una lista ordenada no requiere que recorras
siempre toda la lista. Diseña una función que determine la pertenencia a una lista ordenada
efectuando el menor número posible de comparaciones y desplazamientos sobre la lista.
· 265 Implementa una función que ordene una lista cualquiera mediante el método de la
burbuja.
· 266 Diseña una función que diga, devolviendo el valor 1 o el valor 0, si una lista está
ordenada o desordenada.
.............................................................................................
lista.c
1 TipoLista concatena_listas(TipoLista a, TipoLista b)
2 {
3 TipoLista c = NULL;
4 struct Nodo * aux , * nuevo, * anterior = NULL;
5
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 267 Diseña una función que añada a una lista una copia de otra lista.
· 268 Diseña una función que devuelva una lista con los elementos de otra lista que sean
mayores que un valor dado.
· 269 Diseña una función que devuelva una lista con los elementos comunes a otras dos listas.
· 270 Diseña una función que devuelva una lista que es una copia invertida de otra lista.
.............................................................................................
lista.c
1 TipoLista libera_lista(TipoLista lista)
2 {
3 struct Nodo *aux , *otroaux ;
4
5 aux = lista;
6 while (aux != NULL) {
7 otroaux = aux ->sig;
8 free(aux );
9 aux = otroaux ;
10 }
11 return NULL;
12 }
lista.c
1 void libera_lista(TipoLista * lista)
2 {
3 struct Nodo *aux , *otroaux ;
4
5 aux = *lista;
6 while (aux != NULL) {
7 otroaux = aux ->sig;
8 free(aux );
9 aux = otroaux ;
10 }
11 *lista = NULL;
12 }
De este modo nos aseguramos de que el puntero lista fija su valor a NULL.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 271 Diseña una función que devuelva un ((corte)) de la lista. Se proporcionarán como
parámetros dos enteros i y j y se devolverá una lista con una copia de los nodos que ocu-
pan las posiciones i a j − 1, ambas incluı́das.
· 272 Diseña una función que elimine un ((corte)) de la lista. Se proporcionarán como parámetros
dos enteros i y j y se eliminarán los nodos que ocupan las posiciones i a j − 1, ambas incluı́das.
.............................................................................................
lista.h lista.h
1 struct Nodo {
2 int info;
3 struct Nodo * sig;
4 };
5
lista.c lista.c
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include "lista.h"
4
5 TipoLista lista_vacia(void)
6 {
7 return NULL;
8 }
9
19 nuevo->info = valor ;
20 nuevo->sig = lista;
21 lista = nuevo;
22 return lista;
23 }
24
38 return lista;
39 }
40
45 if (lista != NULL) {
46 aux = lista->sig;
47 free(lista);
48 lista = aux ;
49 }
50 return lista;
51 }
52
57 if (lista != NULL) {
58 for (atras = NULL, aux = lista; aux ->sig != NULL; atras = aux , aux = aux ->sig) ;
59 free(aux );
60 if (atras == NULL)
61 lista = NULL;
62 else
63 atras->sig = NULL;
64 }
65 return lista;
66 }
67
82 printf ("->");
83 for (aux = lista; aux != NULL; aux = aux ->sig)
84 printf ("[%d]->", aux ->info);
85 printf ("|\n");
86 }
87
101
102 for (atras = NULL, aux =lista; aux != NULL; atras = aux , aux = aux ->sig)
103 if (aux ->info == valor ) {
104 if (atras == NULL)
105 lista = aux ->sig;
106 else
107 atras->sig = aux ->sig;
108 free(aux );
109 return lista;
110 }
111 return lista;
112 }
113
148 for (i=0, atras=NULL, aux =lista; i < pos && aux != NULL; i++, atras = aux , aux = aux ->sig) ;
149 nuevo->sig = aux ;
150 if (atras == NULL)
151 lista = nuevo;
152 else
153 atras->sig = nuevo;
154 return lista;
155 }
156
164 for (atras = NULL, aux = lista; aux != NULL; atras = aux , aux = aux ->sig)
165 if (valor <= aux ->info) {
166 /* Aquı́ insertamos el nodo entre atras y aux. */
167 nuevo->sig = aux ;
168 if (atras == NULL)
169 lista = nuevo;
170 else
171 atras->sig = nuevo;
172 /* Y como ya está insertado, acabamos. */
173 return lista;
174 }
175 /* Si llegamos aquı́, es que nuevo va al final de la lista. */
176 nuevo->sig = NULL;
177 if (atras == NULL)
178 lista = nuevo;
179 else
180 atras->sig = nuevo;
181 return lista;
182 }
183
3 #include "lista.h"
4
5 int main(void)
6 {
7 TipoLista l, l2, l3;
8
88 printf ("Creación de una nueva lista con los elementos 30, 40, 50\n");
89 l2 = lista_vacia();
90 l2 = inserta_por_cola(l2, 30);
91 l2 = inserta_por_cola(l2, 40);
92 l2 = inserta_por_cola(l2, 50);
93 muestra_lista(l2);
94
107 return 0;
108 }
?
Pertenece 5 a la lista: 1
?
Pertenece 7 a la lista: 0
Inserción por cola de 1
->[8]->[2]->[1]->[5]->[1]->|
Borrado de primera ocurrencia de 1
->[8]->[2]->[5]->[1]->|
Nuevo borrado de primera ocurrencia de 1
->[8]->[2]->[5]->|
Nuevo borrado de primera ocurrencia de 1 (que no está)
->[8]->[2]->[5]->|
Inserción por cola y por cabeza de 2
->[2]->[8]->[2]->[5]->[2]->|
Borrado de todas las ocurrencias de 2
->[8]->[5]->|
Borrado de todas las ocurrencias de 8
->[5]->|
Inserción de 1 en posición 0
->[1]->[5]->|
Inserción de 10 en posición 2
->[1]->[5]->[10]->|
Inserción de 3 en posición 1
->[1]->[3]->[5]->[10]->|
Inserción de 4, 0, 20 y 5 en orden
->[0]->[1]->[3]->[4]->[5]->[5]->[10]->[20]->|
Creación de una nueva lista con los elementos 30, 40, 50
->[30]->[40]->[50]->|
Concatenación de las dos listas para formar una nueva
->[0]->[1]->[3]->[4]->[5]->[5]->[10]->[20]->[30]->[40]->[50]->|
Liberación de las tres listas
->|
->|
->|
hacer que el nodo que sigue al nuevo nodo sea el que era apuntado por el puntero a cabeza,
No importa cuán larga sea la lista: la inserción por cabeza es siempre igual de rápida. Requiere
una cantidad de tiempo constante. Pero la inserción por cola está seriamente penalizada en
comparación con la inserción por cabeza. Como no sabemos dónde está el último elemento,
hemos de recorrer la lista completa cada vez que deseamos añadir por la cola. Una forma de
eliminar este problema consiste en mantener siempre dos punteros: uno al primer elemento de
la lista y otro al último.
La nueva estructura de datos que representa una lista podrı́a definirse ası́:
6 struct Lista_cc {
7 struct Nodo * cabeza;
8 struct Nodo * cola;
9 };
Podemos representar gráficamente una lista con punteros a cabeza y cola ası́:
Los punteros lista.cabeza y lista.cola forman un único objeto del tipo lista_cc.
Vamos a presentar ahora unas funciones que gestionan listas con punteros a cabeza y cola.
Afortunadamente, todo lo aprendido con las listas del apartado anterior nos vale. Eso sı́, algu-
nas operaciones se simplificarán notablemente (añadir por la cola, por ejemplo), pero otras se
complicarán ligeramente (eliminar la cola, por ejemplo), ya que ahora hemos de encargarnos de
mantener siempre un nuevo puntero (lista.cola) apuntando correctamente al último elemento
de la lista.
y su implementación:
y deseamos insertar el valor 1 en cabeza, basta con modificar lista.cabeza y ajustar el campo
sig del nuevo nodo para que apunte a la antigua cabeza. Como puedes ver, lista.cola sigue
apuntando al mismo lugar al que apuntaba inicialmente:
cabeza info sig info sig info sig info sig
lista 1 3 8 2
cola
Ya está, ¿no? No. Hay un caso en el que también hemos de modificar lista.cola además de
lista.cabeza: cuando la lista está inicialmente vacı́a. ¿Por qué? Porque el nuevo nodo de la lista
será cabeza y cola a la vez.
Fı́jate, si partimos de esta lista:
cabeza
lista
cola
Si sólo modificásemos el valor de lista.cabeza, tendrı́amos esta otra lista mal formada en la que
lista.cola no apunta al último elemento:
cabeza info sig
lista 1
cola
info sig
nuevo
info sig
nuevo 1
info sig
nuevo 1
info sig
nuevo 1
La única precaución que hemos de tener es que, cuando la lista esté inicialmente vacı́a, se
modifique tanto el puntero a la cabeza como el puntero a la cola para que ambos apunten a
nuevo.
lista cabeza cola.c
1 struct Lista_cc inserta_por_cola(struct Lista_cc lista, int valor )
2 {
3 struct Nodo * nuevo;
4
9 if (lista.cola != NULL) {
10 lista.cola->sig = nuevo;
11 lista.cola = nuevo;
12 }
13 else
14 lista.cabeza = lista.cola = nuevo;
15 return lista;
16 }
Fı́jate: la inserción por cola en este tipo de listas es tan eficiente como la inserción por
cabeza. No importa lo larga que sea la lista: siempre cuesta lo mismo insertar por cola, una
cantidad constante de tiempo. Acaba de rendir su primer fruto el contar con punteros a cabeza
y cola.
9 /* Lista con un solo nodo: se borra el nodo y la cabeza y la cola pasan a ser NULL. */
10 if (lista.cabeza == lista.cola) {
11 free(lista.cabeza);
12 lista.cabeza = lista.cola = NULL;
13 return lista;
14 }
15
2. Si la lista tiene un único elemento, liberamos su memoria y hacemos que los punteros a
cabeza y cola apunten a NULL.
aux
b) hacemos que el penúltimo no tenga siguiente nodo (ponemos su campo sig a NULL)
para que ası́ pase a ser el último,
aux
c) liberamos la memoria del que hasta ahora era el último nodo (el apuntado por
lista.cola)
aux
5 /* Lista vacı́a. */
6 if (lista.cabeza == NULL)
7 return lista;
8
Fı́jate en la condición del bucle: detecta si hemos llegado o no al penúltimo nodo preguntando
si el que sigue a aux es el último (el apuntado por lista.cola).
La operación de borrado de la cola no es, pues, tan eficiente como la de borrado de la cabeza,
pese a que tenemos un puntero a la cola. El tiempo que necesita es directamente proporcional
a la longitud de la lista.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 273 Diseña una función que determine si un número pertenece o no a una lista con punteros
a cabeza y cola.
· 274 Diseña una función que elimine el primer nodo con un valor dado en una lista con
punteros a cabeza y cola.
· 275 Diseña una función que elimine todos los nodos con un valor dado en una lista con
punteros a cabeza y cola.
· 276 Diseña una función que devuelva el elemento que ocupa la posición n en una lista con
puntero a cabeza y cola. (La cabeza ocupa la posición 0.) La función devolverá como valor de
retorno 1 o 0 para, respectivamente, indicar si la operación se pudo completar con éxito o si
fracasó. La operación no se puede completar con éxito si n es negativo o si n es mayor o igual
que la talla de la lista. El valor del nodo se devolverá en un parámetro pasado por referencia.
· 277 Diseña una función que devuelva un ((corte)) de la lista. Se recibirán dos ı́ndices i y j y
se devolverá una nueva lista con punteros a cabeza y cola con una copia de los nodos que van
del que ocupa la posición i al que ocupa la posición j − 1, ambos incluı́dos. La lista devuelta
tendrá punteros a cabeza y cola.
· 278 Diseña una función de inserción ordenada en una lista ordenada con punteros a cabeza
y cola.
· 279 Diseña una función que devuelva el menor valor de una lista ordenada con punteros a
cabeza y cola.
· 280 Diseña una función que devuelva el mayor valor de una lista ordenada con punteros a
cabeza y cola.
· 281 Diseña una función que añada a una lista con punteros a cabeza y cola una copia de
otra lista con punteros a cabeza y cola.
.............................................................................................
Una lista es un puntero a un struct DNodo (o a NULL). Nuevamente, definiremos un tipo para
poner énfasis en que un puntero representa a la lista que ((cuelga)) de él.
1 typedef struct DNodo * TipoDLista;
Observa que cada nodo tiene dos punteros: uno al nodo anterior y otro al siguiente. ¿Qué nodo
sigue al último nodo? Ninguno, o sea, NULL. ¿Y cuál antecede al primero? Ninguno, es decir,
NULL.
El caso de la inserción en la lista vacı́a es trivial: se pide memoria para un nuevo nodo cuyos
punteros ant y sig se ponen a NULL y hacemos que la cabeza apunte a dicho nodo.
Aquı́ tienes la función que codifica el método descrito. Hemos factorizado y dispuesto al
principio los elementos comunes al caso general y al de la lista vacı́a:
lista doble.c
1 TipoDLista inserta_por_cabeza(TipoDLista lista, int valor )
2 {
3 struct DNodo * nuevo;
4
10 if (lista != NULL)
11 lista->ant = nuevo;
12
13 lista = nuevo;
14 return lista;
15 }
Te proponemos como ejercicios algunas de las funciones básicas para el manejo de listas
doblemente enlazadas:
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 282 Diseña una función que inserte un nuevo nodo al final de una lista doblemente enlazada.
· 283 Diseña una función que borre la cabeza de una lista doblemente enlazada. Presta
especial atención al caso en el que la lista consta de un sólo elemento.
.............................................................................................
atras aux
atras aux
4. Y se pone el campo sig del que hasta ahora era penúltimo (el apuntado por atras) a NULL.
atras aux
El caso de la lista vacı́a tiene fácil solución: no hay nada que borrar. Es más problemática la
lista con sólo un nodo. El problema con ella estriba en que no hay elemento penúltimo (el anterior
al último es NULL). Tendremos, pues, que detectar esta situación y tratarla adecuadamente.
lista doble.c
1 TipoDLista borra_por_cola(TipoDLista lista)
2 {
3 struct DNodo * aux , * atras;
4
5 /* Lista vacı́a. */
6 if (lista == NULL)
7 return lista;
8
16 /* Caso general. */
17 for (aux =lista; aux ->sig!=NULL; aux =aux ->sig) ;
18 atras = aux ->ant;
19 free(aux );
20 atras->sig = NULL;
21 return lista;
22 }
2. Pedimos memoria para un nuevo nodo, lo apuntamos con el puntero nuevo y le asignamos
el valor:
aux
5. Ojo con este paso, que es complicado. Hacemos que el anterior a aux tenga como siguiente
a nuevo, es decir, aux ->ant->sig = nuevo:
aux
6. Y ya sólo resta que el anterior a aux sea nuevo con la asignación aux ->ant = nuevo:
aux
18 /* Inserción no en cabeza. */
19 nuevo = malloc(sizeof (struct DNodo));
20 nuevo->info = valor ;
21 for (i = 0, aux = lista; i < pos && aux != NULL; i++, aux = aux ->sig) ;
22 if (aux == NULL) /* Inserción por cola. */
23 lista = inserta_por_cola(lista, valor );
24 else {
25 nuevo->sig = aux ;
26 nuevo->ant = aux ->ant;
27 aux ->ant->sig = nuevo;
28 aux ->ant = nuevo;
29 }
30 return lista;
31 }
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 284 Reescribe la función de inserción en una posición dada para que no efectúe llamadas a
la función inserta_por_cabeza.
· 285 Reescribe la función de inserción en una posición dada para que no efectúe llamadas a
la función inserta_por_cola. ¿Es más eficiente la nueva versión? ¿Por qué?
· 286 ¿Qué ocurrirı́a si las últimas lı́neas de la función fueran éstas?:
1 ...
2 nuevo->sig = aux ;
3 nuevo->ant = aux ->ant;
4 aux ->ant = nuevo;
5 aux ->ant->sig = nuevo;
6 }
7 return lista;
8 }
¿Es correcta ahora la función? Haz una traza con un caso concreto.
.............................................................................................
2. Hacemos que el que sigue al anterior de aux sea el siguiente de aux (¡qué galimatı́as!). O
sea, hacemos aux ->ant->sig=aux ->sig:
aux
3. Ahora hacemos que el que antecede al siguiente de aux sea el anterior a aux . Es decir,
aux ->sig->ant=aux ->ant:
aux
aux
Hemos de ser cautos. Hay un par de casos especiales que merecen ser tratados aparte: el
borrado del primer nodo y el borrado del último nodo. Veamos cómo proceder en el primer
caso: tratemos de borrar el nodo de valor 3 en la lista del ejemplo anterior.
1. Una vez apuntado el nodo por aux , sabemos que es el primero porque apunta al mismo
nodo que lista:
aux
2. Hacemos que el segundo nodo deje de tener antecesor, es decir, que el puntero aux ->sig->ant
valga NULL (que, por otra parte, es lo mismo que hacer aux ->sig->ant=aux ->ant):
aux
3. Ahora hacemos que lista pase a apuntar al segundo nodo (lista=aux ->sig):
aux
4. Y por fin, podemos liberar al nodo apuntado por aux (free(aux )):
aux
1. Empezamos por localizarlo con aux y detectamos que efectivamente es el último porque
aux ->sig es NULL:
aux
2. Hacemos que el siguiente del que antecede a aux sea NULL: (aux ->ant->sig=NULL):
aux
lista doble.c
1 TipoDLista borra_primera_ocurrencia(TipoDLista lista, int valor )
2 {
3 struct Nodo * aux ;
4
20 free(aux );
21
22 return lista;
23 }
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 287 Diseña una función que permita efectuar la inserción ordenada de un elemento en una
lista con enlace doble que está ordenada.
· 288 Diseña una función que permita concatenar dos listas doblemente enlazadas. La función
recibirá las dos listas y devolverá una lista nueva con una copia de la primera seguida de una
copia de la segunda.
· 289 Diseña una función que devuelva una copia invertida de una lista doblemente enlazada.
.............................................................................................
con puntero a cabeza y cola permiten realizar operaciones de inserción por cola en un número
constante de pasos. Aún ası́, hay operaciones de cola que también son ineficientes en esta última
estructura de datos: la eliminación del nodo de cola, por ejemplo, sigue necesitando un tiempo
proporcional a la longitud de la lista.
La estructura que presentamos en esta sección, la lista doblemente enlazada con puntero
a cabeza y cola, corrige la ineficiencia en el borrado del nodo de cola. Una lista doblemente
enlazada con puntero a cabeza y cola puede representarse gráficamente ası́:
cabeza ant info sig ant info sig ant info sig
lista 3 8 2
cola
La definición del tipo es fácil ahora que ya hemos estudiado diferentes tipos de listas:
lista doble cc.h
1 struct DNodo {
2 int info;
3 struct DNodo * ant;
4 struct DNodo * sig;
5 };
6
7 struct DLista_cc {
8 struct DNodo * cabeza;
9 struct DNodo * cola;
10 }
11
Sólo vamos a presentarte una de las operaciones sobre este tipo de listas: el borrado de la
cola. El resto de operaciones te las proponemos como ejercicios.
Con cualquiera de las otras estructuras de datos basadas en registros enlazados, el borrado
del nodo de cola no podı́a efectuarse en tiempo constante. Ésta lo hace posible. ¿Cómo? Lo
mejor es que, una vez más, despleguemos los diferentes casos y estudiemos ejemplos concretos
cuando convenga:
hacemos lo siguiente:
a) localizamos al penúltimo elemento, que es lista.cola->ant, y lo mantenemos apuntado
con un puntero auxiliar aux :
aux
cabeza ant info sig ant info sig ant info sig
lista 3 8 2
cola
6 if (lista.cabeza == lista.cola) {
7 free(lista.cabeza);
8 lista.cabeza = lista.cola = NULL;
9 return lista;
10 }
11
12 aux = lista.cola->ant;
13 free(lista.cola);
14 aux ->sig = NULL;
15 lista.cola = aux ;
16 return lista;
17 }
Ha sido fácil, ¿no? No ha hecho falta bucle alguno. La operación se ejecuta en un número de
pasos que es independiente de lo larga que sea la lista.
Ahora te toca a tı́ desarrollar código. Practica con estos ejercicios:
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 290 Diseña una función que calcule la longitud de una lista doblemente enlazada con pun-
teros a cabeza y cola.
· 291 Diseña una función que permita insertar un nuevo nodo en cabeza.
· 292 Diseña una función que permita insertar un nuevo nodo en cola.
· 293 Diseña una función que permita borrar el nodo de cabeza.
· 294 Diseña una función que elimine el primer elemento de la lista con un valor dado.
· 295 Diseña una función que elimine todos los elementos de la lista con un valor dado.
· 296 Diseña una función que inserte un nodo en una posición determinada que se indica por
su ı́ndice.
· 297 Diseña una función que inserte ordenadamente en una lista ordenada.
· 298 Diseña una función que muestre por pantalla el contenido de una lista, mostrando el
valor de cada celda en una lı́nea. Los elementos se mostrarán en el mismo orden con el que
aparecen en la lista.
· 299 Diseña una función que muestre por pantalla el contenido de una lista, mostrando el
valor de cada celda en un lı́nea. Los elementos se mostrarán en orden inverso.
· 300 Diseña una función que devuelva una copia invertida de una lista doblemente enlazada
con puntero a cabeza y cola.
.............................................................................................
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 301 Rellena una tabla similar a la anterior para estas otras operaciones:
· 302 Vamos a montar una pila con listas. La pila es una estructura de datos en la que sólo
podemos efectuar las siguientes operaciones:
¿Qué tipo de lista te parece más adecuado para implementar una pila? ¿Por qué?
· 303 Vamos a montar una cola con listas. La cola es una estructura de datos en la que sólo
podemos efectuar las siguientes operaciones:
¿Qué tipo de lista te parece más adecuado para construir una cola? ¿Por qué?
.............................................................................................
1. Añadir un disco.
7. Finalizar.
A priori no sabemos cuántas canciones hay en un disco, ni cuántos discos hay que almacenar
en la base de datos, ası́ que utilizaremos listas para ambas entidades. Nuestra colección será,
pues, una lista de discos que, a su vez, contienen listas de canciones. No sólo eso: no queremos
que nuestra aplicación desperdicie memoria con cadenas que consumen más memoria que la
necesaria, ası́ que usaremos memoria dinámica también para la reserva de memoria para cadenas.
Lo mejor es dividir el problema en estructuras de datos claramente diferenciadas (una para
la lista de discos y otra para la lista de canciones) y diseñar funciones para manejar cada una de
ellas. Atención al montaje que vamos a presentar, pues es el más complicado de cuantos hemos
estudiado.
1 struct Cancion {
2 char * titulo;
3 struct Cancion * sig;
4 };
5
8 struct Disco {
9 char * titulo;
10 char * interprete;
11 int anyo;
12 TipoListaCanciones canciones;
13 };
14
E x p r e s s i o n \0 J o h n C o l t r a n e \0
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
T a n g e r i n e D r e a m \0
0 1 2 3 4 5 0 1 2 3 4 5 6 7
L o g o s \0 I g n a c i o \0
0 1 2 3 4 5 6 7 8
V a n g e l i s \0
titulo sig titulo sig titulo sig
0 1 2 3 4 5 0 1 2 3 4 5 6 7
titulo sig
L o g o s \0 I g n a c i o \0
0 1 2 3 4 5 6 0 1 2 3 4 5 6 7 8
titulo sig
O g u n d e \0 D o m i n i o n \0
0 1 2 3 4 5 0 1 2 3 4 5 6 7 8
titulo sig
T o b e \0 O f f e r i n g \0
0 1 2 3 4 5 6 7 8 9 10 0 1 2 3 4 5 6 7 8 9 10
E x p r e s s i o n \0 N u m b e r o n e \0
Empezaremos por diseñar la estructura que corresponde a una lista de canciones. Después
nos ocuparemos del diseño de registros del tipo ((disco compacto)). Y acabaremos definiendo un
tipo ((colección de discos compactos)).
Vamos a diseñar funciones para gestionar listas de canciones. Lo que no vamos a hacer es
montar toda posible operación sobre una lista. Sólo invertiremos esfuerzo en las operaciones que
se van a utilizar. Éstas son:
Pasemos a la función que añade una canción a una lista de canciones. No nos indican que las
canciones deban almacenarse en un orden determinado, ası́ que recurriremos al método más
sencillo: la inserción por cabeza.
1 TipoListaCanciones anyade_cancion(TipoListaCanciones lista, char titulo[])
2 {
3 struct Cancion * nuevo = malloc(sizeof (struct Cancion));
4
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 304 La verdad es que insertar las canciones por la cabeza es el método menos indicado,
pues cuando se recorra la lista para mostrarlas por pantalla aparecerán en orden inverso a aquél
con el que fueron introducidas. Modifica anyade_cancion para que las canciones se inserten por
la cola.
· 305 Y ya que sugerimos que insertes canciones por cola, modifica las estructuras necesarias
para que la lista de canciones se gestione con una lista de registros con puntero a cabeza y cola.
.............................................................................................
Mostrar la lista de canciones es muy sencillo:
1 void muestra_canciones(TipoListaCanciones lista)
2 {
3 struct Cancion * aux ;
4
Buscar una canción es un simple recorrido que puede terminar anticipadamente tan pronto
se encuentra el objeto buscado:
1 int contiene_cancion_con_titulo(TipoListaCanciones lista, char titulo[])
2 {
3 struct Cancion * aux ;
4
Borrar todas las canciones de una lista debe liberar la memoria propia de cada nodo, pero
también debe liberar la cadena que almacena cada tı́tulo, pues también se solicitó con malloc:
1 TipoListaCanciones libera_canciones(TipoListaCanciones lista)
2 {
3 struct Cancion * aux , * siguiente;
4
5 aux = lista;
6 while (aux != NULL) {
7 siguiente = aux ->sig;
8 free(aux ->titulo);
9 free(aux );
10 aux = siguiente;
11 }
12 return NULL;
13 }
No ha sido tan difı́cil. Una vez sabemos manejar listas, las aplicaciones prácticas se diseñan
reutilizando buena parte de las rutinas que hemos presentado en apartados anteriores.
Pasamos a encargarnos de las funciones que gestionan la lista de discos. Como es habitual,
empezamos con una función que crea una colección (una lista) vacı́a:
1 TipoColeccion crea_coleccion(void)
2 {
3 return NULL;
4 }
Añadir un disco obliga a solicitar memoria tanto para el registro en sı́ como para algunos
de sus componentes: el tı́tulo y el intérprete:
1 TipoColeccion anyade_disco(TipoColeccion lista, char titulo[], char interprete[],
2 int anyo, TipoListaCanciones canciones)
3 {
4 struct Disco * disco;
5
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 306 Modifica anyade_disco para que los discos estén siempre ordenados alfabéticamente
por intérprete y, para cada intérprete, por valor creciente del año de edición.
.............................................................................................
Y la memoria solicitada debe liberarse ı́ntegramente: si al reservar memoria para un disco
ejecutamos tres llamadas a malloc, habrá que efectuar tres llamadas a free:
1 TipoColeccion libera_coleccion(TipoColeccion lista)
2 {
3 struct Disco * aux , * siguiente;
4
5 aux = lista;
6 while (aux != NULL) {
7 siguiente = aux ->sig;
8 free(aux ->titulo);
9 free(aux ->interprete);
10 aux ->canciones = libera_canciones(aux ->canciones);
11 free(aux );
12 aux = siguiente;
13 }
14 return NULL;
15 }
La función de búsqueda por tı́tulo de canción es similar, sólo que llama a la función que busca
una canción en una lista de canciones:
1 struct Disco * busca_disco_por_titulo_cancion(TipoColeccion coleccion, char titulo[])
2 {
3 struct Disco * aux ;
4
Sólo nos queda por definir la función que elimina un disco de la colección dado su tı́tulo:
6 for (atras = NULL, aux =coleccion; aux != NULL; atras = aux , aux = aux ->sig)
7 if (strcmp(aux ->titulo, titulo) == 0 && strcmp(aux ->interprete, interprete) == 0) {
8 if (atras == NULL)
9 coleccion = aux ->sig;
10 else
11 atras->sig = aux ->sig;
12 free(aux ->titulo);
13 free(aux ->interprete);
14 aux ->canciones = libera_canciones(aux ->canciones);
15 free(aux );
16 return coleccion;
17 }
18 return coleccion;
19 }
discoteca2.c discoteca2.c
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <string.h>
4 #include <ctype.h>
5
.
.
.
182
195 do {
196 printf ("Menú\n");
197 printf ("1) A~nadir disco\n");
198 printf ("2) Buscar por tı́tulo del disco\n");
199 printf ("3) Buscar por intérprete\n");
200 printf ("4) Buscar por tı́tulo de canción\n");
201 printf ("5) Mostrar todo\n");
202 printf ("6) Eliminar un disco por tı́tulo e intérprte\n");
203 printf ("7) Finalizar\n");
204 printf ("Opción: "); gets(linea); sscanf (linea, "%d", &opcion);
205
206 switch(opcion) {
207 case Anyadir :
208 printf ("Tı́tulo: "); gets(titulo_disco);
209 printf ("Intérprete: "); gets(interprete);
210 printf ("A~
no: "); gets(linea); sscanf (linea, "%d", &anyo);
262 return 0;
263 }
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 307 Modifica el programa para que se almacene la duración de cada canción (en segundos)
junto al tı́tulo de la misma.
· 308 La función de búsqueda de discos por intérprete se detiene al encontrar el primer
disco de un intérprete dado. Modifica la función para que devuelva una lista con una copia
de todos los discos de un intérprete. Usa esa lista para mostrar su contenido por pantalla con
muestra_coleccion y elimı́nala una vez hayas mostrado su contenido.
· 309 Diseña una aplicación para la gestión de libros de una biblioteca. Debes mantener
dos listas: una lista de libros y otra de socios. De cada socio recordamos el nombre, el DNI
y el teléfono. De cada libro mantenemos los siguientes datos: tı́tulo, autor, ISBN, código de
la biblioteca (una cadena con 10 caracteres) y estado. El estado es un puntero que, cuando
vale NULL, indica que el libro está disponible y, en caso contrario, apunta al socio al que se ha
prestado el libro.
El programa debe permitir dar de alta y baja libros y socios, ası́ como efectuar el préstamo
de un libro a un socio y gestionar su devolución. Ten en cuenta que no es posible dar de baja
a un socio que posee un libro en préstamo ni dar de baja un libro prestado.
.............................................................................................
Las listas circulares, por ejemplo, son listas sin final. El nodo siguiente al que parece el
último nodo es el primero. Ningún nodo está ligado a NULL.
Este tipo de estructura de datos es útil, por ejemplo, para mantener una lista de tareas
a las que hay que ir dedicando atención rotativamente: cuando hemos hecho una ronda,
queremos pasar nuevamente al primer elemento. El campo sig del último elemento permite
pasar directamente al primero, con lo que resulta sencillo codificar un bucle que recorre
rotativamente la lista.
En muchas aplicaciones es preciso trabajar con matrices dispersas. Una matriz dispersa es
una matriz en la que muy pocos componentes presentan un valor diferente de cero. Esta
matriz, por ejemplo, es dispersa:
0 0 2.5 0 0 1.2 0 0 0 0
0 0 0 0 0 0 0 0 0 0
0 3.7 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0
0 1.3 8.1 0 0 0 0 0.2 0 0
0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0
De los 100 componentes de esta matriz de 10 × 10, tan sólo hay 6 no nulos. Las matri-
ces dispersas pueden representarse con listas de listas para ahorrar memoria. Una lista
mantiene las filas que, a su vez, son listas de valores no nulos. En estas últimas listas,
cada nodo almacena la columna del valor no nulo y el propio valor. La matriz dispersa
del ejemplo se representarı́a ası́ (suponiendo que filas y columnas empiezan numerándose
en 1, como es habitual en matemáticas):
matriz
columna sig
2
sig fila cols valor
3 3.7
der info izq der info izq der info izq der info izq
1 6 12 23
Una ventaja de los árboles binarios de búsqueda es la rapidez con que pueden resolver
la pregunta ((¿pertenece un valor determinado al conjunto de valores del árbol?)). Hay un
método recursivo que recibe un puntero a un nodo y dice:
• si el puntero vale NULL; la respuesta es no;
• si el valor coincide con el del nodo apuntado, la respuesta es sı́;
• si el valor es menor que el valor del nodo apuntado, entonces la respuesta la conoce
el hijo izquierdo, por lo que se le pregunta a él (recursivamente);
• y si el valor es mayor que el valor del nodo apuntado, entonces la respuesta la conoce
el hijo derecho, por lo que se le pregunta a él (recursivamente).
Ingenioso, ¿no? Observa que muy pocos nodos participan en el cálculo de la respuesta. Si
deseas saber, por ejemplo, si el 6 pertenece al árbol de la figura, sólo hay que preguntarle
a los nodos que tienen el 10, el 3 y el 6. El resto de nodos no se consultan para nada.
Siempre es posible responder a una pregunta de pertenencia en un árbol con n nodos
visitando un número de nodos que es, a lo sumo, igual a 1 + log2 n. Rapidı́simo. ¿Qué
costará, a cambio, insertar o borrar un nodo en el árbol? Cabe pensar que mucho más
que un tiempo proporcional al número de nodos, pues la estructura de los enlaces es muy
compleja. Pero no es ası́. Existen procedimientos sofisticados que consiguen efectuar esas
operaciones en tiempo proporcional ¡al logaritmo en base 2 del número de nodos!
Hay muchas más estructuras de datos que permiten acelerar sobremanera los programas
que gestionan grandes conjuntos de datos. Apenas hemos empezado a conocer y aprendido a
manejar las herramientas con las que se construyen los programas: las estructuras de datos y
los algoritmos.
Ficheros
—Me temo que sı́, señora —dijo Alicia—. No recuerdo las cosas como solı́a. . . ¡y no
conservo el mismo tamaño diez minutos seguidos!
Lewis Carroll, Alicia en el Paı́s de las Maravillas.
Acabamos nuestra introducción al lenguaje C con el mismo objeto de estudio con el que finaliza-
mos la presentación del lenguaje Python: los ficheros. Los ficheros permiten guardar información
en un dispositivo de almacenamiento de modo que ésta ((sobreviva)) a la ejecución de un pro-
grama. No te vendrı́a mal repasar los conceptos introductorios a ficheros antes de empezar.
1 2 1 0 0
1 2 1 0 0
1 2 \t 1 0 0
1 2 \n 1 0 0
1 2 \n 1 0 0 \n
Las herramientas con las que leemos los datos de ficheros de texto saben lidiar con las compli-
caciones que introducen estos separadores blancos repetidos.
Los ficheros de texto cuentan con la ventaja de que se pueden inspeccionar con ayuda de
un editor de texto y permiten ası́, por lo general, deducir el tipo de los diferentes datos que lo
componen, pues éstos resultan legibles.
00001100
Pero si optamos por almacenarlo como un int, serán cuatro los bytes escritos:
11111111
tiene dos interpretaciones posibles: el valor 255 si entendemos que es un dato de tipo char o el
valor −1 si consideramos que codifica un dato de tipo unsigned char.1
Como puedes ver, la secuencia de bits que escribimos en el fichero es exactamente la misma
que hay almacenada en la memoria, usando la mismı́sima codificación binaria. De ahı́ el nombre
de ficheros binarios.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 310 ¿Qué ocupa en un fichero de texto cada uno de estos datos?
a) 1 d) -15 g) -32768
b) 0 e) 128 h) 2147483647
c) 12 f) 32767 i) -2147483648
¿Y cuánto ocupa cada uno de ellos si los almacenamos en un fichero binario como valores
de tipo int?
1 Un fichero de texto no presentarı́a esta ambigüedad: el número se habrı́a escrito como −1 o como 255. Sı́
que presentarı́a, sin embargo, un punto de elección reservado al programador: aunque −1 lleva signo y por tanto
se almacenará en una variable de algún tipo con signo, ¿queremos almacenarlo en una variable de tipo char,
una variable de tipo int o, por qué no, en una variable de tipo float?
· 311 ¿Cómo se interpreta esta secuencia de bytes en cada uno de los siguientes supuestos?
Portabilidad de ficheros
Los ficheros binarios presentan algunos problemas de portabilidad, pues no todos los or-
denadores almacenan en memoria los valores numéricos de la misma forma: los ficheros
binarios escritos en un ordenador ((big-endian)) no son directamente legibles en un ordenador
((little-endian)).
Los ficheros de texto son, en principio, más portables, pues la tabla ASCII es un estándar
ampliamente aceptado para el intercambio de ficheros de texto. No obstante, la tabla ASCII
es un código de 7 bits que sólo da cobertura a los sı́mbolos propios de la escritura del inglés
y algunos caracteres especiales. Los caracteres acentuados, por ejemplo, están excluidos. En
los últimos años se ha intentado implantar una familia de estándares que den cobertura a
estos y otros caracteres. Como 8 bits resultan insuficientes para codificar todos los caracteres
usados en la escritura de cualquier lenguaje, hay diferentes subconjuntos para cada una de
las diferentes comunidades culturales. Las lenguas románicas occidentales usan el estándar
IsoLatin-1 (o ISO-8859-1), recientemente ampliado con el sı́mbolo del euro para dar lugar
al IsoLatin-15 (o ISO-8859-15). Los problemas de portabilidad surgen cuando interpretamos
un fichero de texto codificado con IsoLatin-1 como si estuviera codificado con otro estándar:
no veremos más que un galimatı́as de sı́mbolos extraños allı́ donde se usan caracteres no
ASCII.
1. Se abre el fichero en modo lectura, escritura, adición, o cualquier otro modo válido.
2. Se trabaja con él leyendo o escribiendo datos, según el modo de apertura escogido. Al
abrir un fichero se dispone un ((cabezal)) de lectura o escritura en un punto definido del
fichero (el principio o el final). Cada acción de lectura o escritura desplaza el cabezal de
izquierda a derecha, es decir, de principio a final del fichero.
3. Se cierra el fichero.
Bueno, lo cierto es que, como siempre en C, hay un paso adicional y previo a estos tres: la
declaración de una variable de ((tipo fichero)). La cabecera stdio.h incluye la definición de
un tipo de datos llamado FILE y declara los prototipos de las funciones de manipulación de
ficheros. Nuestra variable de tipo fichero ha de ser un puntero a FILE , es decir, ha de ser de
tipo FILE *.
Las funciones básicas con las que vamos a trabajar son:
fopen: abre un fichero. Recibe la ruta de un fichero (una cadena) y el modo de apertura
(otra cadena) y devuelve un objeto de tipo FILE *.
Los modos de apertura para ficheros de texto con los que trabajaremos son éstos:
• "r" (lectura): El primer carácter leı́do es el primero del fichero.
• "w" (escritura): Trunca el fichero a longitud 0. Si el fichero no existe, se crea.
• "a" (adición): Es un modo de escritura que preserva el contenido original del fichero.
Los caracteres escritos se añaden al final del fichero.
Si el fichero no puede abrirse por cualquier razón, fopen devuelve el valor NULL. (Observa
que los modos se indican con cadenas, no con caracteres: debes usar comillas dobles.)
fclose: cierra un fichero. Recibe el FILE * devuelto por una llamada previa a fopen.
El valor devuelto por fclose es un código de error que nos advierte de si hubo un fallo al
cerrar el fichero. El valor 0 indica éxito y el valor EOF (predefinido en stdio.h) indica error.
Más adelante indicaremos cómo obtener información adicional acerca del error detectado.
Cada apertura de un fichero con fopen debe ir acompañada de una llamada a fclose una
vez se ha terminado de trabajar con el fichero.
fscanf : lee de un fichero. Recibe un fichero abierto con fopen (un FILE *), una cadena
de formato (usando las marcas de formato que ya conoces por scanf ) y las direcciones
de memoria en las que debe depositar los valores leı́dos. La función devuelve el número
de elementos efectivamente leı́dos (valor que puedes usar para comprobar si la lectura se
completó con éxito).
fprintf : escribe en un fichero. Recibe un fichero abierto con fopen (un FILE *), una cade-
na de formato (donde puedes usar las marcas de formato que aprendiste a usar con printf )
y los valores que deseamos escribir. La función devuelve el número de caracteres efectiva-
mente escritos (valor que puedes usar para comprobar si se escribieron correctamente los
datos).
Como puedes ver no va a resultar muy difı́cil trabajar con ficheros de texto en C. A fin de
cuentas, las funciones de escritura y lectura son básicamente idénticas a printf y scanf , y ya
hemos aprendido a usarlas. La única novedad destacable es la nueva forma de detectar si hemos
llegado al final de un fichero o no: ya no se devuelve la cadena vacı́a como consecuencia de una
lectura al final del fichero, como ocurrı́a en Python, sino que hemos de preguntar explı́citamente
por esa circunstancia usando una función (feof ).
Nada mejor que un ejemplo para aprender a utilizar ficheros de texto en C. Vamos a generar
los 1000 primeros números primos y a guardarlos en un fichero de texto. Cada número se
escribirá en una lı́nea.
3 int es_primo(int n)
4 {
5 int i, j, primo;
6 primo = 1;
7 for (j=2; j<=n/2; j++)
8 if (n % j == 0) {
9 primo = 0;
10 break;
11 }
12 return primo;
13 }
14
15 int main(void)
16 {
17 FILE * fp;
18 int i, n;
19
20 fp = fopen("primos.txt", "w");
21 i = 1;
22 n = 0;
23 while (n<1000) {
24 if (es_primo(i)) {
25 fprintf (fp, "%d\n", i);
26 n++;
27 }
28 i++;
29 }
30 fclose(fp);
31
32 return 0;
33 }
Hemos llamado a la variable de fichero fp por ser abreviatura del término ((file pointer))
(puntero a fichero). Es frecuente utilizar ese nombre para las variables de tipo FILE *.
Una vez compilado y ejecutado el programa genera primos obtenemos un fichero de texto
llamado primos.txt del que te mostramos sus primeras y últimas lı́neas (puedes comprobar la
corrección del programa abriendo el fichero primos.txt con un editor de texto):
primos.txt
1 1
2 2
3 3
4 5
5 7
6 11
7 13
8 17
9 19
10 23
.
.
.
990 7823
991 7829
992 7841
993 7853
994 7867
995 7873
996 7877
997 7879
998 7883
999 7901
1000 7907
Aunque en pantalla lo vemos como una secuencia de lı́neas, no es más que una secuencia de
caracteres:
1 \n 2 \n 3 \n 5 \n ... 7 9 0 1 \n 7 9 0 7 \n
Diseñemos ahora un programa que lea el fichero primos.txt generado por el programa
anterior y muestre por pantalla su contenido:
1 #include <stdio.h>
2
3 int main(void)
4 {
5 FILE * fp;
6 int i;
7
8 fp = fopen("primos.txt", "r");
9 fscanf (fp, "%d", &i);
10 while ( ! feof (fp) ) {
11 printf ("%d\n", i);
12 fscanf (fp, "%d", &i);
13 }
14 fclose(fp);
15
16 return 0;
17 }
Observa que la llamada a fscanf se encuentra en un bucle que se lee ası́ ((mientras no se
haya acabado el fichero. . . )), pues feof averigua si hemos llegado al final del fichero. La lı́nea 9
contiene una lectura de datos para que la consulta a feof tenga sentido: feof sólo actualiza
su valor tras efectuar una operación de lectura del fichero. Si no te gusta la aparición de dos
sentencias fscanf , puedes optar por esta alternativa:
1 #include <stdio.h>
2
3 int main(void)
4 {
5 FILE * fp;
6 int i;
7
8 fp = fopen("primos.txt", "r");
9 while (1) {
10 fscanf (fp, "%d", &i);
11 if (feof (fp)) break;
12 printf ("%d\n", i);
13 }
14 fclose(fp);
15
16 return 0;
17 }
3 int main(void)
4 {
5 FILE * fp;
6 int i;
7
8 fp = fopen("primos.txt", "r");
9 do {
10 fscanf (fp, "%d", &i);
11 if (!feof (fp))
12 printf ("%d\n", i);
13 } while (!feof (fp));
14 fclose(fp);
15
16 return 0;
17 }
¿Y si el fichero no existe?
Al abrir un fichero puede que detectes un error: fopen devuelve la dirección NULL. Hay varias
razones, pero una que te ocurrirá al probar algunos de los programas del texto es que el
fichero que se pretende leer no existe. Una solución puede consistir en crearlo en ese mismo
instante:
1 f = fopen(ruta, "r");
2 if (f == NULL) {
3 f = fopen(ruta, "w");
4 fclose(f );
5 f = fopen(ruta, "r");
6 }
Si el problema era la inexistencia del fichero, este truco funcionará, pues el modo "w" lo
crea cuando no existe.
Es posible, no obstante, que incluso este método falle. En tal caso, es probable que tengas
un problema de permisos: ¿tienes permiso para leer ese fichero?, ¿tienes permiso para escribir
en el directorio en el que reside o debe residir el fichero? Más adelante prestaremos atención
a esta cuestión.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 312 Diseña un programa que añada al fichero primos.txt los 100 siguientes números pri-
mos. El programa leerá el contenido actual del fichero para averiguar cuál es el último primo del
fichero. A continuación, abrirá el fichero en modo adición ("a") y añadirá 100 nuevos primos. Si
ejecutásemos una vez genera primos y, a continuación, dos veces el nuevo programa, el fichero
acabarı́a conteniendo los 1200 primeros primos.
· 313 Diseña un programa que lea de teclado una frase y escriba un fichero de texto llamado
palabras.txt en el que cada palabra de la frase ocupa una lı́nea.
· 314 Diseña un programa que lea de teclado una frase y escriba un fichero de texto llamado
letras.txt en el que cada lı́nea contenga un carácter de la frase.
· 315 Modifica el programa miniGalaxis para que gestione una lista de records. Un fichero
de texto, llamado minigalaxis.records almacenará el nombre y número de movimientos de
los 5 mejores jugadores de todos los tiempos (los que completaron el juego usando el menor
número de sondas).
· 316 Disponemos de dos ficheros: uno contiene un diccionario y el otro, un texto. El diccio-
nario está ordenado alfabéticamente y contiene una palabra en cada lı́nea. Diseña un programa
que lea el diccionario en un vector de cadenas y lo utilice para detectar errores en el texto. El
programa mostrará por pantalla las palabras del texto que no están en el diccionario, indicando
los números de lı́nea en que aparecen.
Supondremos que el diccionario contiene, a lo sumo, 1000 palabras y que la palabra más
larga (tanto en el diccionario como en el texto) ocupa 30 caracteres.
(Si quieres usar un diccionario real como el descrito y trabajas en Unix, encontrarás uno en
inglés en /usr/share/dict/words o /usr/dict/words. Puedes averiguar el número de palabras
que contiene con el comando wc de Unix.)
· 317 Modifica el programa del ejercicio anterior para que el número de palabras del vector que
las almacena se ajuste automáticamente al tamaño del diccionario. Tendrás que usar memoria
dinámica.
Si usas un vector de palabras, puedes efectuar dos pasadas de lectura en el fichero que
contiene el diccionario: una para contar el número de palabras y saber ası́ cuánta memoria es
necesaria y otra para cargar la lista de palabras en un vector dinámico. Naturalmente, antes de
la segunda lectura deberás haber reservado la memoria necesaria.
Una alternativa a leer dos veces el fichero consiste en usar realloc juiciosamente: reserva
inicialmente espacio para, digamos, 1000 palabras; si el diccionario contiene un número de
palabras mayor que el que cabe en el espacio de memoria reservada, duplica la capacidad del
vector de palabras (cuantas veces sea preciso si el problema se da más de una vez).
Otra posibilidad es usar una lista simplemente enlazada, pues puedes crearla con una primera
lectura. Sin embargo, no es recomendable que sigas esta estrategia, pues no podrás efectuar una
búsqueda dicotómica a la hora de determinar si una palabra está incluida o no en el diccionario.
.............................................................................................
Ya vimos en su momento que fscanf presenta un problema cuando leemos cadenas: sólo
lee una ((palabra)), es decir, se detiene al llegar a un blanco. Aprendimos a usar entonces una
función, gets, que leı́a una lı́nea completa. Hay una función equivalente para ficheros de texto:
char * fgets(char cadena[], int max_tam, FILE * fichero );
¡Ojo con el prototipo de fgets! ¡El parámetro de tipo FILE * es el último, no el primero! Otra
incoherencia de C. El primer parámetro es la cadena en la que se desea depositar el resultado de
la lectura. El segundo parámetro, un entero, es una medida de seguridad: es el máximo número
de bytes que queremos leer en la cadena. Ese lı́mite permite evitar peligrosos desbordamientos
de la zona de memoria reservada para cadena cuando la cadena leı́da es más larga de lo previsto.
El último parámetro es, finalmente, el fichero del que vamos a leer (previamente se ha abierto
con fopen). La función se ocupa de terminar correctamente la cadena leı́da con un ’\0’, pero
respetando el salto de lı́nea (\n) si lo hubiera.2 En caso de querer suprimir el retorno de lı́nea,
puedes invocar una función como ésta sobre la cadena leı́da:
1 void quita_fin_de_linea(char linea[])
2 {
3 int i;
4 for (i=0; linea[i] != ’\0’; i++)
5 if (linea[i] == ’\n’) {
6 linea[i] = ’\0’;
2 En esto se diferencia de gets.
7 break;
8 }
9 }
La función fgets devuelve una cadena (un char *). En realidad, es un puntero a la propia
variable cadena cuando todo va bien, y NULL cuando no se ha podido efectuar la lectura. El valor
de retorno es útil, únicamente, para hacer detectar posibles errores tras llamar a la función.
Hay más funciones de la familia get. La función fgetc, por ejemplo, lee un carácter:
int fgetc(FILE * fichero);
No te equivoques: devuelve un valor de tipo int, pero es el valor ASCII de un carácter. Puedes
asignar ese valor a un unsigned char, excepto cuando vale EOF (de ((end of file))), que es una
constante (cuyo valor es −1) que indica que no se pudo leer el carácter requerido porque llegamos
al final del fichero.
Las funciones fgets y fgetc se complementan con fputs y fputc, que en lugar de leer una
cadena o un carácter, escriben una cadena o un carácter en un fichero abierto para escritura o
adición. He aquı́ sus prototipos:
int fputs(char cadena[], FILE * fichero);
int fputc(int caracter , FILE * fichero);
Al escribir una cadena con fputs, el terminador ’\0’ no se escribe en el fichero. Pero no te
preocupes: fgets ((lo sabe)) y lo introduce automáticamente en el vector de caracteres al leer del
fichero.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 318 Hemos escrito este programa para probar nuestra comprensión de fgets y fputs (presta
atención también a los blancos, que se muestran con el carácter ):
1 #include <stdio.h>
2 #include <string.h>
3
12 f = fopen("prueba.txt", "w");
13 fputs("si", f );
14 fputs("no\n", f );
15 fclose(f );
16
17 f = fopen("prueba.txt", "r");
18 aux = fgets(s, MAXLON, f );
19 printf ("%s %s\n", aux , s);
20 aux = fgets(s, MAXLON, f );
21 printf ("%s %s\n", aux , s);
22 fclose(f );
23
24 return 0;
25 }
Primera cuestión: ¿Cuántos bytes ocupa el fichero prueba.txt?
Al ejecutarlo, obtenemos este resultado en pantalla:
sino
sino
(null) sino
Segunda cuestión: ¿Puedes explicar con detalle qué ha ocurrido? (El texto (((null))) es
escrito automáticamente por printf cuando se le pasa como cadena un puntero a NULL.)
.............................................................................................
Una agenda
Vamos a desarrollar un pequeño ejemplo centrado en las rutinas de entrada/salida para la ges-
tión de una agenda montada con una lista simplemente enlazada. En la agenda, que cargaremos
de un fichero de texto, tenemos el nombre, la dirección y el teléfono de varias personas. Cada
entrada en la agenda se representará con tres lı́neas del fichero de texto. He aquı́ un ejemplo de
fichero con este formato:
agenda.txt agenda.txt
1 Juan Gil
2 Ronda Mijares, 1220
3 964 123456
4 Ana Garcı́a
5 Plaza del Sol, 13
6 964-872777
7 Pepe Pérez
8 Calle de Arriba, 1
9 964 263 263
Nuestro programa podrá leer en memoria los datos de un fichero como éste y también escribirlos
en fichero desde memoria.
Las estructuras de datos que manejaremos en memoria se definen ası́:
1 struct Entrada {
2 char * nombre;
3 char * direccion;
4 char * telefono;
5 };
6
7 struct NodoAgenda {
8 struct Entrada datos;
9 struct NodoAgenda * sig;
10 };
11
Al final del apartado presentamos el programa completo. Centrémonos ahora en las funciones
de escritura y lectura del fichero. La rutina de escritura de datos en un fichero recibirá la
estructura y el nombre del fichero en el que guardamos la información. Guardaremos cada
entrada de la agenda en tres lı́neas: una por cada campo.
1 void escribe_agenda(TipoAgenda agenda, char nombre_fichero[])
2 {
3 struct NodoAgenda * aux ;
4 FILE * fp;
5
6 fp = fopen(nombre_fichero, "w");
7 for (aux =agenda; aux !=NULL; aux =aux ->sig)
8 fprintf (fp, "%s\n%s\n%s\n", aux ->datos.nombre,
9 aux ->datos.direccion,
10 aux ->datos.telefono);
11 fclose(fp);
12 }
3 TipoAgenda agenda;
4 struct Entrada * entrada_leida;
5 FILE * fp;
6 char nombre[MAXCADENA+1], direccion[MAXCADENA+1], telefono[MAXCADENA+1];
7 int longitud ;
8
9 agenda = crea_agenda();
10
11 fp = fopen(nombre_fichero, "r");
12 while (1) {
13 fgets(nombre, MAXCADENA, fp);
14 if (feof (fp)) break; // Si se acabó el fichero, acabar la lectura.
15 quita_fin_de_linea(nombre);
16
27 return agenda;
28 }
8 struct Entrada {
9 char * nombre;
10 char * direccion;
11 char * telefono;
12 };
13
14 struct NodoAgenda {
15 struct Entrada datos;
16 struct NodoAgenda * sig;
17 };
18
34 {
35 printf ("Nombre : %s\n", e->datos.nombre);
36 printf ("Dirección: %s\n", e->datos.direccion);
37 printf ("Teléfono : %s\n", e->datos.telefono);
38 }
39
44 free(e->datos.nombre);
45 free(e->datos.direccion);
46 free(e->datos.telefono);
47 free(e);
48 }
49
50
51 TipoAgenda crea_agenda(void)
52 {
53 return NULL;
54 }
55
94 return NULL;
95 }
96
151
152 /************************************************************************
153 * Programa principal
154 ************************************************************************/
156
169 do {
170 printf ("Menú:\n");
171 printf ("1) Ver contenido completo de la agenda.\n");
172 printf ("2) Dar de alta una persona.\n");
173 printf ("3) Buscar teléfonos de una persona.\n");
174 printf ("4) Salir.\n");
175 printf ("Opción: ");
176 gets(linea); sscanf (linea, "%d", &opcion);
177
178 switch(opcion) {
179
202
206 return 0;
207 }
discoteca.txt
1 Expression
2 John Coltrane
3 1972
4 Ogunde
5 To be
6 Offering
7 Expression
8 Number One
9 Logos
10 Tangerine Dream
11 1982
12 Logos
13 Dominion
14 Ignacio
15 Vangelis
16 1977
17 Ignacio
Pero hay un serio problema: ¿cómo sabe el programa dónde empieza y acaba cada disco? El
programa no puede distinguir entre el tı́tulo de una canción, el de un disco o el nombre de un
intérprete. Podrı́amos marcar cada lı́nea con un par de caracteres que nos indiquen qué tipo de
información mantiene:
discoteca.txt
1 TD Expression
2 IN John Coltrane
3 A~
N 1972
4 TC Ogunde
5 TC To be
6 TC Offering
7 TC Expression
8 TC Number One
9 TD Logos
10 IN Tangerine Dream
11 A~
N 1982
12 TC Logos
13 TC Dominion
14 TD Ignacio
15 IN Vangelis
16 A~
N 1977
17 TC Ignacio
19 1
20 Ignacio
22 coleccion = crea_coleccion();
23 f = fopen(nombre_fichero, "r");
24 while(1) {
25 fgets(titulo_disco, MAXCAD, f );
26 if (feof (f ))
27 break;
28 quita_fin_de_linea(titulo_disco);
29 fgets(interprete, MAXCAD, f );
30 quita_fin_de_linea(interprete);
31 fgets(linea, MAXCAD, f ); sscanf (linea, "%d", &anyo);
32 fgets(linea, MAXCAD, f ); sscanf (linea, "%d", &numcanciones);
33 lista_canciones = crea_lista_canciones();
34 for (i=0; i<numcanciones; i++) {
35 fgets(titulo_cancion, MAXCAD, f );
36 quita_fin_de_linea(titulo_cancion);
37 lista_canciones = anyade_cancion(lista_canciones, titulo_cancion);
38 }
39 coleccion = anyade_disco(coleccion, titulo_disco, interprete, anyo, lista_canciones);
40 }
41 fclose(f );
42
43 return coleccion;
44 }
La detección del final de fichero se ha de hacer tras una lectura infructuosa, por lo que la
hemos dispuesto tras el primer fgets del bucle.
La lectura de lı́neas con fgets hace que el salto de lı́nea esté presente, ası́ que hay que
eliminarlo explı́citamente.
Al guardar el fichero hemos de asegurarnos de que escribimos la información en el mismo
formato:
1 void guarda_coleccion(TipoColeccion coleccion, char nombre_fichero[])
2 {
3 struct Disco * disco;
8 f = fopen(nombre_fichero, "w");
9 for (disco = coleccion; disco != NULL; disco = disco->sig) {
10 fprintf (f , "%s\n", disco->titulo);
11 fprintf (f , "%s\n", disco->interprete);
12 fprintf (f , "%d\n", disco->anyo);
13
14 numcanciones = 0;
15 for (cancion = disco->canciones; cancion != NULL; cancion = cancion->sig)
16 numcanciones++;
17 fprintf (f , "%d\n", numcanciones);
18
253
254
267 do {
268 printf ("Menú\n");
269 printf ("1) A~nadir disco\n");
270 printf ("2) Buscar por tı́tulo del disco\n");
271 printf ("3) Buscar por intérprete\n");
272 printf ("4) Buscar por tı́tulo de canción\n");
273 printf ("5) Mostrar todo\n");
274 printf ("6) Eliminar un disco por tı́tulo e intérprete\n");
275 printf ("7) Finalizar\n");
276 printf ("Opción: "); gets(linea); sscanf (linea, "%d", &opcion);
.
.
.
331
335 return 0;
336 }
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 319 La gestión de ficheros mediante su carga previa en memoria puede resultar problemática
al trabajar con grandes volúmenes de información. Modifica el programa de la agenda para que
no cargue los datos en memoria. Todas la operaciones (añadir datos y consultar) se efectuarán
gestionando directamente ficheros.
· 320 Modifica el programa propuesto en el ejercicio anterior para que sea posible borrar
entradas de la agenda. (Una posible solución pasa por trabajar con dos ficheros, uno original
y uno para copias, de modo que borrar una información sea equivalente a no escribirla en la
copia.)
· 321 Modifica el programa de la agenda para que se pueda mantener más de un teléfono
asociado a una persona. El formato del fichero pasa a ser el siguiente:
Una lı́nea que empieza por la letra N contiene el nombre de una persona.
Una lı́nea que empieza por la letra D contiene la dirección de la persona nombre cuyo
nombre acaba de aparecer.
Una lı́nea que empieza por la letra T contiene un número de teléfono asociado a la persona
cuyo nombre apareció más recientemente en el fichero.
Ten en cuenta que no se puede asociar más de una dirección a una persona (y si eso ocurre en
el fichero, debes notificar la existencia de un error), pero sı́ más de un teléfono. Además, puede
haber lı́neas en blanco (o formadas únicamente por espacios en blanco) en el fichero. He aquı́
un ejemplo de fichero con el nuevo formato:
agenda.txt
1 N Juan Gil
2 D Ronda Mijares, 1220
3 T 964 123456
4
5 N Ana Garcı́a
6 D Plaza del Sol, 13
7 T 964-872777
8 T 964-872778
9
10
11 N Pepe Pérez
12 D Calle de Arriba, 1
13 T 964 263 263
14 T 964 163 163
15 T 96 2663 663
· 322 En un fichero matriz.mat almacenamos los datos de una matriz de enteros con el
siguiente formato:
Cada una de las restantes lı́neas contiene tantos enteros (separados por espacios) como
indica el número de columnas. Hay tantas lı́neas de este estilo como filas tiene la matriz.
matriz.txt
1 3 4
2 1 0 3 4
3 0 -1 12 -1
4 3 0 99 -3
Escribe un programa que lea matriz.mat efectuando las reservas de memoria dinámica que
corresponda y muestre por pantalla, una vez cerrado el fichero, el contenido de la matriz.
· 323 Modifica el programa del ejercicio anterior para que, si hay menos lı́neas con valores de
filas que filas declaradas en la primera lı́nea, se rellene el restante número de filas con valores
nulos.
Aquı́ tienes un ejemplo de fichero con menos filas que las declaradas:
matriz incompleta.txt
1 3 4
2 1 0 3 4
· 324 Diseña un programa que facilite la gestión de una biblioteca. El programa permitirá
prestar libros. De cada libro se registrará al menos el tı́tulo y el autor. En cualquier instante se
podrá volcar el estado de la biblioteca a un fichero y cargarlo de él.
Conviene que la biblioteca sea una lista de nodos, cada uno de los cuales representa un
libro. Uno de los campos del libro podrı́a ser una cadena con el nombre del prestatario. Si dicho
nombre es la cadena vacı́a, se entenderá que el libro está disponible.
.............................................................................................
Permisos Unix
Los ficheros Unix llevan asociados unos permisos con los que es posible determinar qué
usuarios pueden efectuar qué acciones sobre cada fichero. Las acciones son: leer, escribir y
ejecutar (esta última limitada a ficheros ejecutables, es decir, resultantes de una compilación
o que contienen código fuente de un lenguaje interpretado y siguen cierto convenio). Se
puede fijar cada permiso para el usuario ((propietario)) del fichero, para los usuarios de su
mismo grupo o para todos los usuarios del sistema.
Cuando ejecutamos el comando ls con la opción -l, podemos ver los permisos codifi-
cados con las letras rwx y el carácter -:
-rw-r--r-- 1 usuario migrupo 336 may 12 10:43 kk.c
-rwxr-x--- 1 usuario migrupo 13976 may 12 10:43 a.out
El fichero kk.c tiene permiso de lectura y escritura para el usuario (caracteres 2 a 4), de
sólo lectura para los usuarios de su grupo (caracteres 5 a 7) y de sólo lectura para el resto
de usuarios (caracteres 8 a 10). El fichero a.out puede ser leı́do, modificado y ejecutado
por el usuario. Los usuarios del mismo grupo pueden leerlo y ejecutarlo, pero no modificar
su contenido. El resto de usuarios no puede acceder al fichero.
El comando Unix chmod permite modificar los permisos de un fichero. Una forma tradi-
cional de hacerlo es con un número octal que codifica los permisos. Aquı́ tienes un ejemplo
de uso:
$ chown 0700 a.out
$ ls -l a.out
-rwx------ 1 usuario migrupo 13976 may 12 10:43 a.out
El valor octal 0700 (que en binario es 111000000), por ejemplo, otorga permisos de
lectura, escritura y ejecución al propietario del fichero, y elimina cualquier permiso para el
resto de usuarios. De cada 3 bits, el primero fija el permiso de lectura, el segundo el de
escritura y el tercero el de ejecución. Los 3 primeros bits corresponden al usuario, los tres
siguientes al grupo y los últimos 3 al resto. Ası́ pues, 0700 equivale a -rwx------ en la
notación de ls -l.
Por ejemplo, para que a.out sea también legible y ejecutable por parte de cualquier
miembro del grupo del propietario puedes usar el valor 0750 (que equivale a -rwxr-x---).
¿Qué es stderr ? En principio es también la pantalla, pero podrı́a ser, por ejemplo un fichero en
el que deseamos llevar un cuaderno de bitácora con las anomalı́as o errores detectados durante
la ejecución del programa.
La función printf es una forma abreviada de llamar a fprintf sobre stdout y scanf encubre
una llamada a fscanf sobre stdin. Por ejemplo, estas dos llamadas son equivalentes:
printf ("Esto es la %s\n", "pantalla");
f printf ( stdout, "Esto es la %s\n", "pantalla");
selecciona entrada.c
1 #include <stdio.h>
2
3 int main(void)
4 {
5 FILE * fp;
6 char dedonde[80], nombre[80];
7 int n;
8
19 ...
20 fscanf (fp, "%d", &n); /* Lee de fichero o teclado. */
21 ...
22 if (fp != stdin)
23 fclose(fp);
24 ...
25
26 return 0;
27 }
Existe otra forma de trabajar con fichero o teclado que es más cómoda para el programador:
usando la capacidad de redirección que facilita el intérprete de comandos Unix. La idea consiste
en desarrollar el programa considerando sólo la lectura por teclado y, cuando iniciamos la ejecu-
ción del programa, redirigir un fichero al teclado. Ahora verás cómo. Fı́jate en este programa:
pares.c
1 #include <stdio.h>
2
3 int main(void)
4 {
5 int i, n;
6
1 #include <stdio.h>
2 #include <stdlib.h>
3
4 int main(void)
5 {
6 char a[] = "123";
7 int b;
8
9 b = atoi(a);
10
13 return 0;
14 }
Si deseas interpretar el texto como un float, puedes usar atof en lugar de atoi. Ası́ de fácil.
12
13 return 0;
14 }
Si lo compilas para generar un programa pares, lo ejecutas e introduces los siguientes 10 números
enteros, obtendrás este resultado en pantalla:
$ pares
3
5
6
[6]
7
2
[2]
10
[10]
2
[2]
1
3
13
Cada vez que el ordenador ha detectado un número par, lo ha mostrado en pantalla entre
corchetes.
Creemos ahora, con la ayuda de un editor de texto, numeros.txt, un fichero de texto con
los mismos 10 números enteros que hemos introducido por teclado antes:
numeros.txt
1 3
2 5
3 6
4 7
5 2
6 10
7 2
8 1
9 3
10 13
El carácter < indica a Unix que lea del fichero numeros.txt en lugar de leer del teclado. El
programa, sin tocar una sola lı́nea, pasa a leer los valores de numeros.txt y muestra por pantalla
los que son pares.
También podemos redirigir la salida (la pantalla) a un fichero. Fı́jate:
$ pares < numeros.txt > solopares.txt
Ahora el programa se ejecuta sin mostrar texto alguno por pantalla y el fichero solopares.txt
acaba conteniendo lo que debiera haberse mostrado por pantalla.
$ cat solopares.txt
[6]
[2]
[10]
[2]
Para redirigir la salida de errores, puedes usar el par de caracteres 2> seguido del nombre
del fichero en el que se escribirán los mensajes de error.
La capacidad de redirigir los dispositivos de entrada, salida y errores tiene infinidad de apli-
caciones. Una evidente es automatizar la fase de pruebas de un programa durante su desarrollo.
En lugar de escribir cada vez todos los datos que solicita un programa para ver si efectúa correc-
tamente los cálculos, puedes preparar un fichero con los datos de entrada y utilizar redirección
para que el programa los lea automáticamente.
13 if (c == EOF && nc == 0)
14 return EOF;
15
16 linea[nc] = ’\0’;
17 return nc;
18 }
Para leer una cadena en un vector de caracteres con una capacidad máxima de 100
caracteres, haremos:
lee_linea(cadena, 100);
El valor de cadena se modificará para contener la cadena leı́da. La cadena más larga leı́da
tendrá una longitud de 99 caracteres (recuerda que el ’\0’ ocupa uno de los 100).
Pero hay una posibilidad aún más sencilla: usar fgets sobre stdin:
Una salvedad: fgets incorpora a la cadena leı́da el salto de lı́nea, cosa que gets no hace.
La primera versión, no obstante, sigue teniendo interés, pues te muestra un ((esqueleto))
de función útil para un control detallado de la lectura por teclado. Inspirándote en ella
puedes escribir, por ejemplo, una función que sólo lea dı́gitos, o letras, o texto que satisface
alguna determinada restricción.
La consulta de teclado
La función getc (o, para el caso, fgetc actuando sobre stdin) bloquea la ejecución del
programa hasta que el usuario teclea algo y pulsa la tecla de retorno. Muchos programadores
se preguntan ¿cómo puedo saber si una tecla está pulsada o no sin quedar bloqueado? Ciertas
aplicaciones, como los videojuegos, necesitan efectuar consultas al estado del teclado no
bloqueantes. Malas noticias: no es un asunto del lenguaje C, sino de bibliotecas especı́ficas.
El C estándar nada dice acerca de cómo efectuar esa operación.
En Unix, la biblioteca curses, por ejemplo, permite manipular los terminales y acceder de
diferentes modos al teclado. Pero no es una biblioteca fácil de (aprender a) usar. Y, además,
presenta problemas de portabilidad, pues no necesariamente está disponible en todos los
sistemas operativos.
Cosa parecida podemos decir de otras cuestiones: sonido, gráficos tridimensionales, in-
terfaces gráficas de usuario, etc. C, en tanto que lenguaje de programación estandarizado,
no ofrece soporte. Eso sı́: hay bibliotecas para infinidad de campos de aplicación. Tendrás
que encontrar la que mejor se ajusta a tus necesidades y. . . ¡estudiar!
int fread ( void * direccion, int tam, int numdatos, FILE * fichero );
Los bytes leı́dos se almacenan a partir de direccion. Devuelve el número de datos que ha
conseguido leer (y si ese valor es menor que numdatos, es porque hemos llegado al final
del fichero y no se ha podido efectuar la lectura completa).
fwrite: recibe una dirección de memoria, el número de bytes que ocupa un dato, el número
de datos a escribir y un fichero. Este es su prototipo:
int fwrite( void * direccion, int tam, int numdatos, FILE * fichero );
Escribe en el fichero los tam por numdatos bytes existentes desde direccion en adelante.
Devuelve el número de datos que ha conseguido escribir (si vale menos que numdatos,
hubo algún error de escritura).
Empezaremos a comprender cómo trabajan estas funciones con un sencillo ejemplo. Vamos
a escribir los diez primeros números enteros en un fichero:
diez enteros.c diez enteros.c
1 #include <stdio.h>
2
3 int main(void)
4 {
5 FILE * fp;
6 int i;
7
8 fp = fopen("primeros.dat", "wb");
9 for (i=0; i<10; i++)
10 fwrite(&i, sizeof (int), 1, fp);
11 fclose(fp);
12
13 return 0;
14 }
3 Más
adelante te presentamos tres modos de apertura adicionales.
4 Bueno,
casi. El prototipo no usa el tipo int, sino size t, que está definido como unsigned int. Preferimos
presentarte una versión modificada del prototipo para evitar introducir nuevos conceptos.
3 int main(void)
4 {
5 FILE * fp;
6 int i, v[10];
7
14 return 0;
15 }
Ahora estamos pasando la dirección en la que empieza un vector (v es una dirección, ası́ que no
hemos de poner un & delante), el tamaño de un elemento del vector (sizeof (int)) y el número
de elementos del vector (10). El efecto es que se escriben en el fichero los 40 bytes de memoria
que empiezan donde empieza v. Resultado: todo el vector se almacena en disco con una sola
operación de escritura. Cómodo, ¿no?
Ya te dijimos que la información de todo fichero binario ocupa exactamente el mismo número
de bytes que ocuparı́a en memoria. Hagamos la prueba. Veamos con ls -l, desde el intérprete
de comandos de Unix, cuánto ocupa el fichero:
$ ls -l primeros.dat
-rw-r--r-- 1 usuario migrupo 40 may 10 11:00 primeros.dat
Efectivamente, ocupa exactamente 40 bytes (el número que aparece en quinto lugar). Si lo
mostramos con cat, no sale nada con sentido en pantalla.
$ cat primeros.dat
$
¿Por qué? Porque cat interpreta el fichero como si fuera de texto, ası́ que encuentra la siguiente
secuencia binaria:
1 00000000 00000000 00000000 00000000
2 00000000 00000000 00000000 00000001
3 00000000 00000000 00000000 00000010
4 00000000 00000000 00000000 00000011
5 00000000 00000000 00000000 00000100
6 ...
Los valores ASCII de cada grupo de 8 bits no siempre corresponden a caracteres visibles, por
lo que no se representan como sı́mbolos en pantalla (no obstante, algunos bytes sı́ tienen efecto
en pantalla; por ejemplo, el valor 9 corresponde en ASCII al tabulador).
Hay una herramienta Unix que te permite inspeccionar un fichero binario: od (abreviatura
de ((octal dump)), es decir, ((volcado octal))).
$ od -l primeros.dat
0000000 0 1 2 3
0000020 4 5 6 7
0000040 8 9
0000050
(La opción -l de od hace que muestre la interpretación como enteros de grupos de 4 bytes.)
¡Ahı́ están los números! La primera columna indica (en hexadecimal) el número de byte del
primer elemento de la fila.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 325 ¿Qué aparecerá en pantalla si mostramos con el comando cat el contenido del fichero
binario otraprueba.dat generado en este programa?:
3 int main(void)
4 {
5 FILE * fp;
6 int i, v[26];
7
14 return 0;
15 }
3 int main(void)
4 {
5 FILE * fp;
6 int i, n ;
7
8 fp = fopen("primeros.dat", "rb");
9 for (i=0; i<10; i++) {
10 fread (&n, sizeof (int), 1, fp);
11 printf ("%d\n", n);
12 }
13 fclose(fp);
14
15 return 0;
16 }
3 int main(void)
4 {
5 FILE * fd ;
6 int i, v[10];
7
8 fp = fopen("primeros.dat", "rb");
14 return 0;
15 }
En los dos programas hemos indicado explı́citamente que ı́bamos a leer 10 enteros, pues
sabı́amos de antemano que habı́a exactamente 10 números en el fichero. Es fácil modificar el
primer programa para que lea tantos enteros como haya, sin conocer a priori su número:
lee todos.c lee todos.c
1 #include <stdio.h>
2
3 int main(void)
4 {
5 FILE * fp;
6 int n;
7
8 fp = fopen("primeros.dat", "rb");
9 fread (&n, sizeof (int), 1, fp);
10 while (!feof (fp)) {
11 printf ("%d\n", n);
12 fread (&n, sizeof (int), 1, fp);
13 }
14 fclose(fp);
15
16 return 0;
17 }
Lo cierto es que hay una forma más idiomática, más común en C de expresar lo mismo:
lee todos2.c lee todos2.c
1 #include <stdio.h>
2
3 int main(void)
4 {
5 FILE * fp;
6 int n;
7
8 f = fopen("primeros.dat", "rb");
9 while ( fread (&n, sizeof (int), 1, fp) == 1 )
10 printf ("%d\n", n);
11 fclose(fp);
12
13 return 0;
14 }
En esta última versión, la lectura de cada entero se efectúa con una llamada a fread en la
condición del while.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 326 Diseña un programa que genere un fichero binario primos.dat con los 1000 primeros
números primos.
· 327 Diseña un programa que añada al fichero binario primos.dat (ver ejercicio anterior) los
100 siguientes números primos. El programa leerá el contenido actual del fichero para averiguar
cuál es el último primo conocido. A continuación, abrirá el fichero en modo adición y añadirá
100 nuevos primos. Si ejecutásemos dos veces el programa, el fichero acabarı́a conteniendo los
1200 primeros primos.
.............................................................................................
No sólo puedes guardar tipos relativamente elementales. También puedes almacenar en disco
tipos de datos creados por ti. Este programa, por ejemplo, lee de disco un vector de puntos, lo
modifica y escribe en el fichero el contenido del vector:
4 struct Punto {
5 float x;
6 float y;
7 };
8
9 int main(void)
10 {
11 FILE * fp;
12 struct Punto v[10];
13 int i;
14
31 return 0;
32 }
Esta otra versión no carga el contenido del primer fichero completamente en memoria en
una primera fase, sino que va leyendo, procesando y escribiendo punto a punto:
1 #include <stdio.h>
2 #include <math.h>
3
4 struct Punto {
5 float x;
6 float y;
7 };
8
9 int main(void)
10 {
11 FILE * fp_entrada, * fp_salida;
12 struct Punto p;
13 int i;
14
27 return 0;
28 }
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 328 Los dos programas anteriores suponen que hay diez puntos en el fichero puntos.dat.
Modifı́calo para que procesen tantos puntos como haya en el fichero.
· 329 Implementa un programa que genere un fichero llamado puntos.dat con 10 elementos
del tipo struct Punto. Las coordenadas de cada punto se generarán aleatoriamente en el rango
[−10, 10]. Usa el último programa para generar el fichero puntos2.dat. Comprueba que contiene
el valor absoluto de los valores de puntos.dat. Si es necesario, diseña un nuevo programa que
muestre por pantalla el contenido de un fichero de puntos cuyo nombre suministra por teclado
el usuario.
.............................................................................................
"r+b": No se borra el contenido del fichero, que debe existir previamente. El ((cabezal))
de lectura/escritura se sitúa al principio del fichero.
El valor desde_donde se fija con una constante predefinida que proporciona una interpretación
distinta a desplazamiento:
SEEK_END: el valor de desplazamiento es un valor absoluto a contar desde el final del fichero.
Por ejemplo, fseek (fp, -1, SEEK_END) nos desplaza al último byte de fp: si a continuación
leyésemos un valor, serı́a el del último byte del fichero. La llamada fseek (fp, 0, SEEK_END)
nos situarı́a fuera del fichero (en el mismo punto en el que estamos si abrimos el fichero
en modo de adición).
3 int main(void)
4 {
5 FILE * fp;
6 int n, bytes_leidos, cero = 0;
7
8 fp = fopen("fichero.dat", "r+b");
9 while (fread (&n, sizeof (int), 1, fp) != 0) {
10 if (n % 2 == 0) { // Si el último valor leı́do es par...
11 fseek (fp, -sizeof (int), SEEK_CUR); // ... damos un paso atrás ...
12 fwrite(&cero, sizeof (int), 1, fp); // ... y sobreescribimos su valor absoluto.
13 }
14 }
15 fclose(fp);
16
17 return 0;
18 }
La segunda función que te presentamos en este apartado es ftell . Este es su prototipo:
int ftell (FILE *fp);
El valor devuelto por la función es la posición en la que se encuentra el ((cabezal)) de lectu-
ra/escritura en el instante de la llamada.
Veamos un ejemplo. Este programa, por ejemplo, crea un fichero y nos dice el número de
bytes del fichero:
cuenta bytes.c cuenta bytes.c
1 #include <stdio.h>
2
3 int main(void)
4 {
5 FILE * fp;
6 int i, pos;
7
8 fp = fopen("prueba.dat", "wb");
9 for (i=0; i<10; i++)
10 fwrite(&i, sizeof (int), 1, fp);
11 fclose(fp);
12
13 fp = fopen("prueba.dat", "rb");
14 fseek (fp, 0, SEEK_END);
15 pos = ftell (fp);
16 printf ("Tama~ no del fichero: %d\n", pos);
17 fclose(fp);
18
19 return 0;
20 }
Fı́jate bien en el truco que permite conocer el tamaño de un fichero: nos situamos al final del
fichero con ftell indicando que queremos ir al ((primer byte desde el final)) (byte 0 con el modo
SEEK_END) y averiguamos a continuación la posición en la que nos encontramos (valor devuelto
por ftell ).
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 330 Diseña una función de nombre rebobina que recibe un FILE * y nos ubica al inicio del
mismo.
· 331 Diseña una función que reciba un FILE * (ya abierto) y nos diga el número de bytes
que ocupa. Al final, la función debe dejar el cursor de lectura/escritura en el mismo lugar en el
que estaba cuando se la llamó.
· 332 Diseña un programa que calcule y muestre por pantalla el máximo y el mı́nimo de los
valores de un fichero binario de enteros.
· 333 Diseña un programa que calcule el máximo de los enteros de un fichero binario y lo
intercambie por el que ocupa la última posición.
· 334 Nos pasan un fichero binario dobles.dat con una cantidad indeterminada de números
de tipo float. Sabemos, eso sı́, que los números están ordenados de menor a mayor. Diseña un
programa que pida al usuario un número y determine si está o no está en el fichero.
En una primera versión, implementa una búsqueda secuencial que se detenga tan pronto
estés seguro de que el número buscado está o no. El programa, en su versión final, deberá
efectuar la búsqueda dicotómicamente (en un capı́tulo anterior se ha explicado qué es una
búsqueda dicotómica).
.............................................................................................
Trabajar con ficheros binarios como si se tratara de vectores tiene ciertas ventajas, pero
también inconvenientes. La ventaja más obvia es la capacidad de trabajar con cantidades in-
gentes de datos sin tener que cargarlas completamente en memoria. El inconveniente más serio
es la enorme lentitud con que se pueden ejecutar entonces los programas. Ten en cuenta que
desplazarse por un fichero con fseek obliga a ubicar el ((cabezal)) de lectura/escritura del disco
duro, una operación que es intrı́nsecamente lenta por comportar operaciones mecánicas, y no
sólo electrónicas.
Si en un fichero binario mezclas valores de varios tipos resultará difı́cil, cuando no imposible,
utilizar sensatamente la función fseek para posicionarse en un punto arbitrario del fichero.
Tenemos un problema similar cuando la información que guardamos en un fichero es de longitud
intrı́nsecamente variable. Pongamos por caso que usamos un fichero binario para almacenar una
lista de palabras. Cada palabra es de una longitud, ası́ que no hay forma de saber a priori en qué
byte del fichero empieza la n-ésima palabra de la lista. Un truco consiste en guardar cada palabra
ocupando tanto espacio como la palabra más larga. Este programa, por ejemplo, pide palabras
al usuario y las escribe en un fichero binario en el que todas las cadenas miden exactamente lo
mismo (aunque la longitud de cada una de ellas sea diferente):
guarda palabras.c guarda palabras.c
1 #include <stdio.h>
2
3 #define MAXLON 80
4
5 int main(void)
6 {
7 char palabra[MAXLON+1], seguir [MAXLON+1];
8 FILE * fp;
9
10 fp = fopen("diccio.dat", "wb");
11 do {
12 printf ("Dame una palabra: "); gets(palabra);
13 fwrite(palabra, sizeof (char), MAXLON, fp);
14 printf ("Pulsa ’s’ para a~ nadir otra."); gets(seguir );
15 } while (strcmp(seguir , "s") == 0);
16 fclose(fp);
17
18 return 0;
19 }
3 #define MAXLON 80
Fı́jate en que el valor devuelto por unpack no es directamente el entero, sino una lista (en
realidad una tupla), por lo que es necesario indexarla para acceder al valor que nos interesa.
La razón de que devuelva una lista es que unpack puede desempaquetar varios valores a
la vez. Por ejemplo, unpack ("iid", cadena) desempaqueta dos enteros y un flotante de
cadena (que debe tener al menos 16 bytes, claro está). Puedes asignar los valores devueltos
a tres variables ası́: a, b, c = unpack ("iid", cadena).
Hemos aprendido, pues, a leer ficheros binarios con Python. ¿Cómo los escribimos?
Siguiendo un proceso inverso: empaquetando primero nuestros ((valores Python)) en cadenas
que los codifican en binario mediante la función pack y escribiendolas con el método write.
Este programa de ejemplo escribe un fichero binario con los números del 0 al 99:
Sólo queda que aprendas a implementar acceso directo a los ficheros binarios con Python.
Tienes disponibles los modos de apertura ’r+’, ’w+’ y ’a+’. Además, el método seek
permite desplazarse a un byte cualquiera del fichero y el método tell indica en qué posición
del fichero nos encontramos.
5 int main(void)
6 {
7 FILE * fp;
8 char palabra[MAXLON+1];
9 int tam;
10
23 return 0;
24 }
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 335 Los dos programas anteriores pueden plantear problemas cuando trabajan con palabras
que tiene 80 caracteres más el terminador. ¿Qué problemas? ¿Cómo los solucionarı́as?
· 336 Diseña un programa que lea una serie de valores enteros y los vaya escribiendo en
un fichero hasta que el usuario introduzca el valor −1 (que no se escribirá en el fichero). Tu
programa debe, a continuación, determinar si la secuencia de números introducida en el fichero
es palı́ndroma.
· 337 Deseamos gestionar una colección de cómics. De cada cómic anotamos los siguientes
datos:
Número: un entero.
Año: un entero.
El programa permitirá:
3. Ver un listado por superhéroe que muestre el tı́tulo de todas sus historias.
4. Ver un listado por año que muestre el superhérore y tı́tulo de todas sus historias.
Diseña un programa que gestione la base de datos teniendo en cuenta que no queremos cargarla
en memoria cada vez que ejecutamos el programa, sino gestionarla directamente sobre disco.
.............................................................................................
5.4. Errores
Algunas de las operaciones con ficheros pueden resultar fallidas (apertura de un fichero cuya
ruta no apunta a ningún fichero existente, cierre de un fichero ya cerrado, etc.). Cuando ası́
ocurre, la función llamada devuelve un valor que indica que se cometió un error, pero ese valor
sólo no aporta información que nos permita conocer el error cometido.
La información adicional está codificada en una variable especial: errno (declarada en
errno.h). Puedes comparar su valor con el de las constantes predefinidas en errno.h para
averiguar qué error concreto se ha cometido:
...
Truncamiento de ficheros
Las funciones estándar de manejo de ficheros no permiten efectuar una operación que puede
resultar necesaria en algunas aplicaciones: eliminar elementos de un fichero. Una forma de
conseguir este efecto consiste en generar un nuevo fichero en el que escribimos sólo aquellos
elementos que no deseamos eliminar. Una vez generado el nuevo fichero, borramos el original
y renombramos el nuevo para que adopte el nombre del original. Costoso.
En Unix puedes recurrir a la función truncate (disponible al incluir la cabecera
unistd.h). El perfil de truncate es éste:
La función recibe el nombre de un fichero (que no debe estar abierto) y el número de bytes
que deseamos conservar. Si la llamada tiene éxito, la función hace que en el fichero sólo
permanezcan los longitud primeros bytes y devuelve el valor 0. En caso contrario, devuelve
el valor −1. Observa que sólo puedes borrar los últimos elementos de un fichero, y no
cualquiera de ellos. Por eso la acción de borrar parte de un fichero recibe el nombre de
truncamiento.
Como manejarte con tantas constantes (algunas con significados un tanto difı́cil de com-
prender hasta que curses asignaturas de sistemas operativos) resulta complicado, puedes usar
una función especial:
void perror (char s[]);
Esta función muestra por pantalla el valor de la cadena s, dos puntos y un mensaje de error que
detalla la causa del error cometido. La cadena s, que suministra el programador, suele indicar el
nombre de la función en la que se detectó el error, ayudando ası́ a la depuración del programa.
Tipos básicos
A.1. Enteros
A.1.1. Tipos
Esta tabla muestra el nombre de cada uno de los tipos de datos para valores enteros (algunos
tienen dos nombres válidos), su rango de representación y el número de bytes (grupos de 8 bits)
que ocupan.
(Como ves, los tipos short int, long int y long long int pueden abreviarse, respectivamente,
como short, long, y long long.)
Un par de curiosidades sobre la tabla de tipos enteros:
Los tipos int y long int ocupan lo mismo (4 bytes) y tienen el mismo rango. Esto es ası́
para el compilador gcc sobre un PC. En una máquina distinta o con otro compilador,
podrı́an ser diferentes: los int podrı́an ocupar 4 bytes y los long int, 8, por ejemplo. En
sistemas más antiguos un int ocupaba 2 bytes y un long int, 4.
El nombre del tipo char es abreviatura de ((carácter)) (((character)), en inglés) y, sin em-
bargo, hace referencia a los enteros de 8 bits, es decir, 1 byte. Los valores de tipo char
son ambivalentes: son tanto números enteros como caracteres.
Es posible trabajar con enteros sin signo en C, es decir, números enteros positivos. La ventaja
de trabajar con ellos es que se puede aprovechar el bit de signo para aumentar el rango positivo
y duplicarlo. Los tipos enteros sin signo tienen el mismo nombre que sus correspondientes tipos
con signo, pero precedidos por la palabra unsigned, que actúa como un adjetivo:
Del mismo modo que podemos ((marcar)) un tipo entero como ((sin signo)) con el adjetivo
unsigned, podemos hacer explı́cito que tiene signo con el adjetivo signed. O sea, el tipo
int puede escribirse también como signed int: son exactamente el mismo tipo, sólo que en el
segundo caso se pone énfasis en que tiene signo, haciendo posible una mejora en la legibilidad
de un programa donde este rasgo sea importante.
A.1.2. Literales
Puedes escribir números enteros en notación octal (base 8) o hexadecimal (base 16). Un número
en notación hexadecimal empieza por 0x. Por ejemplo, 0xff es 255 y 0x0 es 0. Un número en
notación octal debe empezar por un 0 y no ir seguido de una x. Por ejemplo, 077 es 63 y 010
es 8.1
Puedes precisar que un número entero es largo añadiéndole el sufijo L (por ((Long))). Por
ejemplo, 2L es el valor 2 codificado con 32 bits. El sufijo LL (por ((long long))) indica que
el número es un long long int. El literal 2LL, por ejemplo, representa al número entero 2
codificado con 64 bits (lo que ocupa un long long int). El sufijo U (combinado opcionalmente
con L o LL) precisa que un número no tiene signo (la U por ((unsigned))).
Normalmente no necesitarás usar esos sufijos, pues C hace conversiones automáticas de tipo
cuando conviene. Sı́ te hará falta si quieres denotar un número mayor que 2147483647 (o menor
que −2147483648), pues en tal caso el número no puede representarse como un simple int. Por
ejemplo, la forma correcta de referirse a 3000000000 es con el literal 3000000000LL.
C resulta abrumador por la gran cantidad de posibilidades que ofrece. Son muchas formas
diferentes de representar enteros, ¿verdad? No te preocupes, sólo en aplicaciones muy concretas
necesitarás utilizar la notación octal o hexadecimal o tendrás que añadir el sufijo a un literal
para indicar su tipo.
A.2. Flotantes
A.2.1. Tipos
También en el caso de los flotantes tenemos dónde elegir: hay tres tipos diferentes. En esta tabla
te mostramos el nombre, máximo valor absoluto y número de bits de cada uno de ellos:
Tipo Máximo valor absoluto Bytes
38
float 3.40282347·10 4
double 1.7976931348623157·10308 8
long double 1.189731495357231765021263853031·104932 12
1 Lo cierto es que también puede usar notación octal o hexadecimal en Python, aunque en su momento no lo
contamos.
Recuerda que los números expresados en coma flotante presentan mayor resolución en la
cercanı́as del 0, y que ésta es tanto menor cuanto mayor es, en valor absoluto, el número
representado. El número no nulo más próximo a cero que puede representarse con cada uno de
los tipos se muestra en esta tabla:
A.2.2. Literales
Ya conoces las reglas para formar literales para valores de tipo float. Puedes añadir el sufijo F
para precisar que el literal corresponde a un double y el sufijo L para indicar que se trata de
un long double. Por ejemplo, el literal 3.2F es el valor 3.2 codificado como double. Al igual
que con los enteros, normalmente no necesitarás precisar el tipo del literal con el sufijo L, a
menos que su valor exceda del rango propio de los float.
Observa que tanto float como double usan la misma marca de formato para impresión (o
sea, con la función printf y similares).
No pretendemos detallar todas las marcas de formato para flotantes. Tenemos, además,
otras como %E, %F, %g, %G, %LE, %LF, %Lg y %LG. Cada marca introduce ciertos matices que,
en según qué aplicaciones, pueden venir muy bien. Necesitarás un buen manual de referencia
a mano para controlar estos y otros muchos aspectos (no tiene sentido memorizarlos) cuando
ejerzas de programador en C durante tu vida profesional.2
Las marcas de formato para la lectura de datos de tipos flotantes presentan alguna
diferencia:
Tipo Notación convencional
float %f
double %lf
long double %Lf
Observa que la marca de impresión de un double es %f, pero la de lectura es %lf. Es una
incoherencia de C que puede darte algún que otro problema.
A.3. Caracteres
El tipo char, que ya hemos presentado al estudiar los tipos enteros, es, a la vez el tipo con el
que solemos representar caracteres y con el que formamos las cadenas.
A.3.1. Literales
Los literales de carácter encierran entre comillas simples al carácter en cuestión o lo codifican
como un número entero. Es posible utilizar secuencias de escape para indicar el carácter que se
encierra entre comillas.
2 En Unix puedes obtener ayuda acerca de las funciones estándar con el manual en lı́nea. Ejecuta man 3
printf, por ejemplo, y obtendrás una página de manual sobre la función printf , incluyendo información sobre
todas sus marcas de formato y modificadores.
3 int main(void)
4 {
5 int a, c;
6 float b;
7
14 return 0;
15 }
Ejecutemos el programa e introduzcamos los valores 20, 3.0 y 4 pulsando el retorno de carro
tras cada uno de ellos.
Entero a: 20
Flotante b: 3.0
Entero c: 4
El entero a es 20, el flotante b es 3.000000 y el entero c es 4
Perfecto. Para ver qué ha ocurrido paso a paso vamos a representar el texto que escribe
el usuario durante la ejecución como una secuencia de teclas. En este gráfico se muestra qué
ocurre durante la ejecución del primer scanf (lı́nea 8), momento en el que las tres variables
están sin inicializar y el usuario acaba de pulsar las teclas 2, 0 y retorno de carro:
2 0 \n a
En la figura hemos representado los caracteres consumidos en color gris. Fı́jate en que el salto
de lı́nea aún no ha sido consumido.
La ejecución del segundo scanf , el que lee el contenido de b, empieza descartando los blancos
iniciales, es decir, el salto de lı́nea:
2 0 \n a 20
Como no hay más caracteres que procesar, scanf queda a la espera de que el usuario teclee algo
con lo que pueda formar un flotante y pulse retorno de carro. Cuando el usaurio teclea el 3.0
seguido del salto de lı́nea, pasamos a esta nueva situación:
2 0 \n 3 . 0 \n a 20
Ahora, scanf reanuda su ejecución y consume el ’3’, el ’.’ y el ’0’. Como detecta que lo que
sigue no es válido para formar un flotante, se detiene, interpreta los caracteres leı́dos como el
valor flotante 3.0 y lo almacena en la dirección de b:
2 0 \n 3 . 0 \n a 20
&b
b 3.0
Finalmente, el tercer scanf entra en ejecución y empieza por saltarse el salto de lı́nea.
2 0 \n 3 . 0 \n a 20
b 3.0
Acto seguido se detiene, pues no es necesario que el usuario introduzca nuevo texto que procesar.
Entonces el usuario escribe el 4 y pulsa retorno:
2 0 \n 3 . 0 \n 4 \n a 20
b 3.0
2 0 \n 3 . 0 \n 4 \n a 20
b 3.0
&c
c 4
Como puedes apreciar, el último salto de lı́nea no llega a ser consumido, pero eso importa poco,
pues el programa finaliza correctamente su ejecución.
Vamos a estudiar ahora el porqué de un efecto curioso. Imagina que, cuando el programa
pide al usuario el primer valor entero, éste introduce tanto dicho valor como los dos siguientes,
separando los tres valores con espacios en blanco. He aquı́ el resultado en pantalla:
Entero a: 20 3.0 4
Flotante b: Entero c: El entero a es 20, el flotante b es 3.000000 y el entero c es 4
El programa ha leı́do correctamente los tres valores, sin esperar a que el usuario introduzca
tres lı́neas con datos: cuando tenı́a que detenerse para leer el valor de b, no lo ha hecho, pues
((sabı́a)) que ese valor era 3.0; y tampoco se ha detenido al leer el valor de c, ya que de algún
modo ((sabı́a)) que era 4. Veamos paso a paso lo que ha sucedido, pues la explicación es bien
sencilla.
Durante la ejecución del primer scanf , el usuario ha escrito el siguiente texto:
2 0 3 . 0 4 \n a
La ejecución del siguiente scanf no ha detenido la ejecución del programa, pues aún habı́a
caracteres pendientes de procesar en la entrada. Como siempre, scanf se ha saltado el primer
blanco y ha ido encontrando caracteres válidos para ir formando un valor del tipo que se le
indica (en este caso, un flotante). La función scanf ha dejado de consumir caracteres al encontrar
un nuevo blanco, se ha detenido y ha almacenado en b el valor flotante 3.0. He aquı́ el nuevo
estado:
2 0 3 . 0 4 \n a 20
&b
b 3.0
Finalmente, el tercer scanf tampoco ha esperado nueva entrada de teclado: se ha saltado direc-
tamente el siguiente blanco, ha encontrado el carácter ’4’ y se ha detenido porque el carácter
\n que le sigue es un blanco. El valor leı́do (el entero 4) se almacena en c:
2 0 3 . 0 4 \n a 20
b 3.0
&c
c 4
2 0 3 . 0 4 \n a 20
b 3.0
c 4
2 0 3 . 1 2 3 4 5 5 \n
· 339 ¿Qué pasa si el usuario escribe la siguiente secuencia de caracteres como datos de
entrada en la ejecución del programa?
2 0 3 . 1 2 3 \n 4 5 5 \n
· 340 ¿Qué pasa si el usuario escribe la siguiente secuencia de caracteres como datos de
entrada en la ejecución del programa?
2 0 2 4 5 x \n
· 341 ¿Qué pasa si el usuario escribe la siguiente secuencia de caracteres como datos de
entrada en la ejecución del programa?
6 x 2 \n
3 #define TALLA 10
4
5 int main(void)
6 {
7 char a[TALLA+1], b[TALLA+1];
8
13 return 0;
14 }
Si ejecutas el programa y escribes una primera cadena sin blancos, pulsas el retorno de carro,
escribes otra cadena sin blancos y vuelves a pulsar el retorno, la lectura se efectúa como cabe
esperar:
Cadena 1: uno
Cadena 2: dos
La cadena 1 es uno y la cadena 2 es dos
Estudiemos paso a paso lo ocurrido. Ante el primer scanf , el usuario ha escrito lo siguiente:
u n o \n
La función ha empezado a consumir los caracteres con los que ir formando la cadena. Al llegar
al salto de lı́nea se ha detenido sin consumirlo. He aquı́ el nuevo estado de cosas:
u n o \n
0 1 2 3 4 5 6 7 8 9
a u n o \0
(Fı́jate en que scanf termina correctamente la cadena almacenada en a.) Acto seguido se ha
ejecutado el segundo scanf . La función se salta entonces el blanco inicial, es decir, el salto de
lı́nea que aún no habı́a sido consumido.
u n o \n
0 1 2 3 4 5 6 7 8 9
a u n o \0
Como no hay más caracteres, scanf ha detenido la ejecución a la espera de que el usuario teclee
algo. Entonces el usuario ha escrito la palabra dos y ha pulsado retorno de carro:
u n o \n d o s \n
0 1 2 3 4 5 6 7 8 9
a u n o \0
u n o \n d o s \n
0 1 2 3 4 5 6 7 8 9
a u n o \0
0 1 2 3 4 5 6 7 8 9
b d o s \0
u n o \n d o s \n
0 1 2 3 4 5 6 7 8 9
a u n o \0
0 1 2 3 4 5 6 7 8 9
b d o s \0
Compliquemos un poco la situación. ¿Qué ocurre si, al introducir las cadenas, metemos
espacios en blanco delante y detrás de las palabras?
Cadena 1: uno
Cadena 2: dos
La cadena 1 es uno y la cadena 2 es dos
Recuerda que scanf se salta siempre los blancos que encuentra al principio y que se detiene
en el primer espacio que encuentra tras empezar a consumir caracteres válidos. Veámoslo paso
a paso. Empezamos con este estado de la entrada:
u n o \n
u n o \n
A continuación consume los caracteres ’u’, ’n’ y ’o’ y se detiene al detectar el blanco que
sigue:
u n o \n
0 1 2 3 4 5 6 7 8 9
a u n o \0
Cuando se ejecuta, el segundo scanf empieza saltándose los blancos iniciales, que son todos los
que hay hasta el salto de lı́nea (incluı́do éste):
u n o \n
0 1 2 3 4 5 6 7 8 9
a u n o \0
De nuevo, como no hay más que leer, la ejecución se detiene. El usuario teclea entonces nuevos
caracteres:
u n o \n d o s \n
0 1 2 3 4 5 6 7 8 9
a u n o \0
u n o \n d o s \n
0 1 2 3 4 5 6 7 8 9
a u n o \0
u n o \n d o s \n
0 1 2 3 4 5 6 7 8 9
a u n o \0
0 1 2 3 4 5 6 7 8 9
b d o s \0
Ya está.
Imagina ahora que nuestro usuario quiere introducir en a la cadena "uno dos" y en b la
cadena "tres". Aquı́ tienes lo que ocurre al ejecutar el programa
Cadena 1: uno dos
Cadena 2: La cadena 1 es uno y la cadena 2 es dos
u n o d o s \n
La función lee en a los caracteres ’u’, ’n’ y ’o’ y se detiene al detectar un blanco. El nuevo
estado se puede representar ası́:
u n o d o s \n
0 1 2 3 4 5 6 7 8 9
a u n o \0
El segundo scanf entra en juego entonces y ((aprovecha)) lo que aún no ha sido procesado, ası́
que empieza por descartar el blanco inicial y, a continuación, consume los caracteres ’d’, ’o’,
’s’:
u n o d o s \n
0 1 2 3 4 5 6 7 8 9
a u n o \0
0 1 2 3 4 5 6 7 8 9
b d o s \0
¿Ves? La consecuencia de este comportamiento es que con scanf sólo podemos leer palabras
individuales. Para leer una lı́nea completa en una cadena, hemos de utilizar una función distinta:
gets (por ((get string)), que en inglés significa ((obtén cadena))), disponible incluyendo stdio.h
en nuestro programa.
3 #define TALLA 80
4
5 int main(void)
6 {
7 char a[TALLA+1], b[TALLA+1];
8 int i;
9
15 return 0;
16 }
Observa que leemos cadenas con gets y un entero con scanf . Vamos a ejecutar el programa
introduciendo la palabra uno en la primera cadena, el valor 2 en el entero y la palabra dos en
la segunda cadena.
Cadena a: uno
Entero i: 2
Cadena b: La cadena a es uno, el entero i es 2 y la cadena b es
¿Qué ha pasado? No hemos podido introducir la segunda cadena: ¡tan pronto hemos escrito
el retorno de carro que sigue al 2, el programa ha finalizado! Estudiemos paso a paso lo ocurrido.
El texto introducido ante el primer scanf es:
u n o \n
u n o \n
0 1 2 3 4 5 6 7 8 9
a u n o \0
A continuación se ejecuta el scanf con el que se lee el valor de i. El usuario teclea lo siguiente:
u n o \n 2 \n
0 1 2 3 4 5 6 7 8 9
a u n o \0
i 2
La función lee el 2 y encuentra un salto de lı́nea. El estado en el que queda el programa es éste:
u n o \n 2 \n
0 1 2 3 4 5 6 7 8 9
a u n o \0
i 2
Fı́jate bien en qué ha ocurrido: nos hemos quedado a las puertas de procesar el salto de lı́nea.
Cuando el programa pasa a ejecutar el siguiente gets, ¡lee una cadena vacı́a! ¿Por qué? Porque
gets lee caracteres hasta el primer salto de lı́nea, y el primer carácter con que nos encontramos
ya es un salto de lı́nea. Pasamos, pues, a este nuevo estado:
u n o \n 2 \n
0 1 2 3 4 5 6 7 8 9
a u n o \0
i 2
0 1 2 3 4 5 6 7 8 9
b \0
¿Cómo podemos evitar este problema? Una solución posible consiste en consumir la cadena
vacı́a con un gets extra y una variable auxiliar. Fı́jate en este programa:
lee alterno bien.c lee alterno bien.c
1 #include <stdio.h>
2
3 #define TALLA 80
4
5 int main(void)
6 {
7 char a[TALLA+1], b[TALLA+1];
8 int i;
9 char findelinea[TALLA+1]; // Cadena auxiliar. Su contenido no nos importa.
10
16 return 0;
17 }
Hemos introducido una variable extra, findelinea, cuyo único objetivo es consumir lo que scanf
no ha consumido. Gracias a ella, éste es el estado en que nos encontramos justo antes de empezar
la lectura de b:
u n o \n 2 \n
0 1 2 3 4 5 6 7 8 9
a u n o \0
i 2
0 1 2 3 4 5 6 7 8 9
findelinea \0
u n o \n 2 \n d o s \n
0 1 2 3 4 5 6 7 8 9
a u n o \0
i 2
0 1 2 3 4 5 6 7 8 9
findelinea \0
Ahora la lectura de b tiene éxito. Tras ejecutar gets, éste es el estado resultante:
u n o \n 2 \n d o s \n
0 1 2 3 4 5 6 7 8 9
a u n o \0
i 2
0 1 2 3 4 5 6 7 8 9
findelinea \0
0 1 2 3 4 5 6 7 8 9
b d o s \0
3 #define TALLA 80
4
5 int main(void)
6 {
7 char a[TALLA+1], b[TALLA+1];
8 int i;
9 char linea[TALLA+1]; // Cadena auxiliar. Su contenido no nos importa.
10
16 return 0;
17 }
((¡Ah, ya sé!, ¡es un libro del Espejo, naturalmente! Si lo pongo delante de un espejo,
las palabras se verán otra vez al derecho.))
Y éste es el poema que leyó Alicia11 :
JERIGÓNDOR
11. [...] Carroll pasa a continuación a interpretar las palabras de la manera siguiente:
Bryllig [“cocillaba”] (der. del verbo “ Bryl” o “ Broil”); “hora de cocinar la comida;
es decir, cerca de la hora de comer”.
Slythy [“agilimosas”] (voz compuesta por “ Slimy” y “ Lithe”. “Suave y activo”.
Tova. Especie de tejón. Tenı́a suave pelo blanco, largas patas traseras y cuernos cortos
como de ciervo, se alimentaba principalmente de queso.
Gyre [“giroscopar”], verbo (derivado de Gyaour o Giaour, “perro”). “Arañar como
un perro”.
Gymble [“barrenar”], (de donde viene Gimblet [“barrena”]) “hacer agujeros en algo”.
Wave [“larde”] (derivado del verbo “ to swab” [“fregar”] o “soak” [“empapar”]). “Ladera
de una colina” (del hecho de empaparse por acción de la lluvia).
Mimsy (de donde viene Mimserable y Miserable): “infeliz”.
Borogove [“burgovo”], especie extinguida de loro. Carecı́a de alas, tenı́a el pico hacia
arriba, y anidaba bajo los relojes de sol: se alimentaba de ternera.
Mome [“aleca”] (de donde viene Solemome y Solemne). Grave.
Rath [“rasta”]. Especie de tortuga de tierra. Cabeza erecta, boca de tiburón, patas
anteriores torcidas, de manera que el animal caminaba sobre sus rodillas; cuerpo liso de
color verde; se alimentaba de golondrinas y ostras.
Outgrabe [“silbramar”]. Pretérito del verbo Outgribe (emparentado con el antiguo
to Grike o Shrike, del que proceden “ Shreak” [“chillar”] y “ Creak” [“chirriar”]:
“chillaban”.
Por tanto, el pasaje dice literalmente: “Era por la tarde, y los tejones, suaves y activos, hurgaban
y hacı́an agujeros en las laderas; los loros eran muy desdichados, y las graves tortugas proferı́an
chillidos.”