Apuntes · Capítulo 5Programación en C
Ciclos
Ciclos
La semana pasada los programas aprendieron a elegir. Hoy aprenden a repetir. Ya
vieron el problema varias veces: to_binary.c de la semana 3 repite dos líneas ocho
veces, y adder4.c de la semana 4 repite la misma cuenta para cada columna. Además,
ante un dato malo, nuestros programas solo sabían avisar y terminar, porque volver a
pedirlo requiere repetir.
Un ciclo (loop) es una instrucción que repite un bloque. C tiene tres: while,
for y do while. En este capítulo los vemos en ese orden, y aprendemos a razonar
sobre ellos con tres preguntas: cómo empieza, cuándo para y qué avanza en cada vuelta.
Luego vemos cuatro patrones que van a usar todo el curso: contador, acumulador,
centinela y buscar el mayor. Al final leemos un archivo byte por byte, hasta que se
acabe, con getchar.
Lectura. El código de este capítulo está en
code/week-05. Dentro del entorno del curso, entren a esa carpeta y corranmakepara compilar los ejemplos ymake testpara comprobarlos. Los comandos de este capítulo se corren desde ahí. Los programas deerrors/tienen errores a propósito y se compilan a mano, como se muestra en cada caso.
Ruta de lectura.
- Antes de clase (lectura obligatoria, 15 a 20 minutos): la introducción, “Repetir en pseudocódigo y en diagramas de flujo” y “
whileen C”. Es lo mínimo para seguir la clase.- En clase: “
for”, “Salirse por uno”, “do while”, “Cuatro patrones” y “Leer hasta el fin de la entrada congetchar”.- Después de clase: para el laboratorio, “El laboratorio, paso a paso”; para repasar, “Razonar antes de ejecutar”, “Resumen” y “Ejercicios”.
Repetir en pseudocódigo y en diagramas de flujo
En la semana 1 escribieron mientras para contar monedas. Este es el algoritmo:
algoritmo monedas_de_cinco()
leer vuelto
monedas ← 0
mientras vuelto ≥ 500 hacer
vuelto ← vuelto - 500
monedas ← monedas + 1
fin mientras
escribir monedas, vuelto
fin algoritmo
Recuerden la regla: se pregunta, y si la respuesta es sí, se ejecuta el bloque una vez y se vuelve a preguntar. Cada pasada por el bloque se llama vuelta (también se dice iteración). Con un vuelto de S/ 17.80, es decir, 1780 céntimos:
| Vuelta | ¿vuelto ≥ 500? |
vuelto al final de la vuelta |
monedas al final de la vuelta |
|---|---|---|---|
| antes de empezar | 1780 | 0 | |
| 1 | sí | 1280 | 1 |
| 2 | sí | 780 | 2 |
| 3 | sí | 280 | 3 |
| no, y se sale | 280 | 3 |
En el diagrama de flujo de la semana 1, mientras es un rombo con una flecha que
regresa: después del último paso del bloque, la flecha sube otra vez hasta el rombo. La
semana pasada vieron que un si abre dos caminos que más abajo se juntan. En un ciclo,
en cambio, uno de los caminos vuelve atrás. Esa flecha hacia atrás es lo que hace
que sea un ciclo.
Tres preguntas
Para entender cualquier ciclo, hagan estas tres preguntas.
| Pregunta | En monedas_de_cinco |
|---|---|
| Inicio: ¿qué valores tienen las variables antes de la primera pregunta? | vuelto es el dato; monedas vale 0 |
| Parada: ¿cuándo se deja de repetir? | cuando vuelto es menor que 500 |
| Avance: ¿qué cambia en cada vuelta para acercarse a la parada? | vuelto baja 500 |
Si no pueden contestar la tercera, probablemente tienen un ciclo infinito (infinite loop), un ciclo que nunca termina.
Idea clave. Un ciclo se entiende con tres preguntas: inicio, parada y avance. Háganlas antes de ejecutar el programa, no después.
para: repetir con un contador
Muchas veces sabemos de antemano cuántas vueltas queremos. Para eso está para, que
usaron en la semana 3. Esta es la suma de los enteros de 1 a n, primero con mientras
y luego con para:
algoritmo suma_hasta_n()
leer n
suma ← 0
i ← 1
mientras i ≤ n hacer
suma ← suma + i
i ← i + 1
fin mientras
escribir suma
fin algoritmo
algoritmo suma_hasta_n_con_para()
leer n
suma ← 0
para i desde 1 hasta n hacer
suma ← suma + i
fin para
escribir suma
fin algoritmo
Hacen exactamente lo mismo. para se ocupa solo de dar el valor inicial a i y de
aumentarlo en 1. Fíjense en que hasta incluye el último valor: para i desde 1 hasta n da una vuelta con i igual a n. Y si n es menor que 1, no da ninguna.
while en C
while es mientras. Las monedas, en coins.c:
while (change >= 500) {
change = change - 500;
coins = coins + 1;
}
La forma es while (condición) { ... }. Se evalúa la condición, y si es verdadera, se
ejecuta el bloque y se vuelve a evaluar. Igual que en if, 0 es falso y cualquier otro
número es verdadero. Y como en if, todo while lleva llaves en este curso.
$ echo 1780 | build/coins
3 monedas de S/ 5; quedan 280 céntimos
$ echo 300 | build/coins
0 monedas de S/ 5; quedan 300 céntimos
Con 300 la condición es falsa desde el inicio, así que el bloque no se ejecuta ni una
vez. while puede dar cero vueltas, y eso casi siempre es lo correcto.
Un contador y un acumulador
Este es el algoritmo suma_hasta_n, en sum_to_n.c:
int sum = 0;
int i = 1;
while (i <= n) {
sum = sum + i;
i = i + 1;
}
Sigámoslo con n = 4.
| Pregunta | sum después de la vuelta |
i después de la vuelta |
|---|---|---|
| antes de empezar | 0 | 1 |
¿1 <= 4? sí |
1 | 2 |
¿2 <= 4? sí |
3 | 3 |
¿3 <= 4? sí |
6 | 4 |
¿4 <= 4? sí |
10 | 5 |
¿5 <= 4? no, y se sale |
10 | 5 |
Hubo 4 vueltas y 5 preguntas. La condición siempre se evalúa una vez más que las vueltas,
porque la última pregunta es la que dice que no. Por eso, al salir, i vale 5 y no 4.
$ echo 4 | build/sum_to_n
La suma de 1 a 4 es 10
$ echo 0 | build/sum_to_n
La suma de 1 a 0 es 0
Con n = 0 no hay vueltas y la suma queda en 0. Es el caso que hay que probar siempre:
qué pasa cuando el ciclo no da ninguna vuelta.
Formas cortas de asignar
i = i + 1 se escribe tanto que C tiene formas cortas. No hacen nada nuevo; solo se
escriben menos.
| Forma corta | Equivale a |
|---|---|
i++ |
i = i + 1 |
i-- |
i = i - 1 |
sum += x |
sum = sum + x |
n -= x |
n = n - x |
w /= 2 |
w = w / 2 |
También existen *= y %=. En este curso, i++ e i-- van solos, como una
instrucción aparte o en la tercera parte de un for. No los metan dentro de otras
expresiones, porque se leen mal y algunas de esas combinaciones son comportamiento
indefinido.
El ciclo infinito
Si nada cambia la condición, el ciclo no termina. Aquí, errors/no_update.c:
int n = 3;
while (n > 0) {
printf("%d\n", n);
}
n vale 3 siempre, así que la pregunta siempre responde sí. gcc no avisa nada, porque
es C válido. Si lo corren en la terminal, llena la pantalla de números hasta que lo
detengan con Ctrl+C, como vieron en la semana 1.
Para ver solo el comienzo usamos head -n 3, un comando que deja pasar las primeras 3
líneas de lo que recibe y termina. Cuando head termina, el programa que le estaba
escribiendo también se detiene.
$ gcc -Wall -Wextra -std=c17 errors/no_update.c -o build/no_update
$ build/no_update | head -n 3
3
3
3
Error común. Cuando un programa parece colgado, casi siempre es un ciclo infinito. Córtenlo con Ctrl+C y hagan la tercera pregunta: ¿qué línea acerca la condición a ser falsa? Si necesitan ver qué pasa, agreguen un
printfdentro del ciclo.
El acumulador puede dar overflow
Una suma que crece en cada vuelta puede pasarse del mayor int, 2147483647, como vieron
en la semana 3. Esa suma es comportamiento indefinido: el estándar de C no dice qué
pasa. make compila nuestros programas con -g -fsanitize=address,undefined: -fsanitize
agrega revisiones que corren junto con el programa, y -g hace que el aviso diga el archivo
y la línea. Probemos con 70000. Esto no es un error de compilación, sino un aviso del
sanitizer mientras el programa corre, y sin esas opciones no aparecería:
$ echo 70000 | build/sum_to_n
sum_to_n.c:15:13: runtime error: signed integer overflow: 2147450880 + 65536 cannot be represented in type 'int'
La suma de 1 a 70000 es -1844932296
El aviso es el dato importante: en esa vuelta, la suma ya no cabe. El número negativo de la última línea salió en esta corrida y no significa nada; no pueden contar con él en otra computadora ni con otras opciones de compilación. La solución es limitar el dato antes de entrar al ciclo, o elegir un problema cuyo resultado quepa.
for
Casi todos los ciclos con contador tienen la misma forma: dar un valor inicial, preguntar
y avanzar. for junta esas tres partes en una sola línea.
for (inicio; condición; avance) {
cuerpo
}
Se comporta como este while, que es equivalente:
inicio;
while (condición) {
cuerpo
avance;
}
Léanlo con cuidado, porque el orden es la clave:
iniciose ejecuta una sola vez, antes de todo.- Se evalúa
condición. Si es falsa, el ciclo termina. - Se ejecuta el
cuerpo. - Se ejecuta
avance, después del cuerpo. - Se vuelve al paso 2.
Comprobémoslo. for_trace.c usa un for, y for_as_while.c el while equivalente:
int i;
for (i = 0; i < 10; i += 3) {
printf("i vale %d\n", i);
}
printf("Al salir, i vale %d\n", i);
i = 0;
while (i < 10) {
printf("i vale %d\n", i);
i += 3;
}
Los dos escriben lo mismo:
$ build/for_trace
i vale 0
i vale 3
i vale 6
i vale 9
Al salir, i vale 12
$ build/for_as_while
i vale 0
i vale 3
i vale 6
i vale 9
Al salir, i vale 12
i |
¿i < 10? |
Qué pasa |
|---|---|---|
| 0 | sí | escribe y i pasa a 3 |
| 3 | sí | escribe y i pasa a 6 |
| 6 | sí | escribe y i pasa a 9 |
| 9 | sí | escribe y i pasa a 12 |
| 12 | no | se sale |
Cuatro vueltas. Al salir, i vale 12, el primer valor que hace falsa la condición, y no
el 9 que se escribió al final.


De para a for
La traducción de para a for es directa. Lo único que hay que cuidar es el hasta.
| Pseudocódigo | C | Valores de i |
|---|---|---|
para i desde 1 hasta n hacer |
for (int i = 1; i <= n; i++) |
1, 2, …, n |
para i desde 0 hasta n - 1 hacer |
for (int i = 0; i < n; i++) |
0, 1, …, n - 1 |
Como hasta incluye el último valor, se traduce con <=; y hasta n - 1 se escribe
mejor con < n. Nuestro para siempre avanza de 1 en 1. Si necesitan otro paso, como
en for_trace.c, en pseudocódigo se escribe con mientras.
Noten el int dentro del paréntesis. Se puede declarar el contador en la primera parte
del for, y entonces solo existe dentro de ese for: es la misma regla de los bloques
de la semana pasada. Conviene hacerlo así, salvo que necesiten el valor al salir. Por eso
for_trace.c lo declara antes.
La tabla de multiplicar
algoritmo tabla_de_multiplicar()
leer n
para i desde 1 hasta 12 hacer
escribir n, "x", i, "=", n * i
fin para
fin algoritmo
En C, en mult_table.c. El programa rechaza antes los números muy grandes, para que los
productos quepan en un int:
for (int i = 1; i <= 12; i++) {
printf("%d x %2d = %d\n", n, i, n * i);
}
Recuerden que un número entre % y d es el ancho mínimo: %2d completa con un espacio
a la izquierda los números de una cifra, y así las columnas quedan alineadas.
$ echo 7 | build/mult_table | head -n 3
7 x 1 = 7
7 x 2 = 14
7 x 3 = 21
Un binario sin repetir líneas
to_binary.c de la semana 3 repite dos líneas ocho veces y guarda ocho variables, porque
el primer bit que sale de dividir entre 2 es el último que se escribe. Con un ciclo hay
una forma más directa para un número de 8 bits: se recorren los pesos de mayor a menor. Si
el número alcanza para ese peso, el bit es 1 y se le resta el peso.
algoritmo binario_de_ocho_bits()
leer n
peso ← 128
mientras peso ≥ 1 hacer
si n ≥ peso entonces
escribir 1
n ← n - peso
sino
escribir 0
fin si
peso ← peso div 2
fin mientras
fin algoritmo
for (int weight = 128; weight >= 1; weight /= 2) {
if (n >= weight) {
printf("1");
n -= weight;
} else {
printf("0");
}
}
El programa de esta semana también revisa que el número esté entre 0 y 255. Sigamos el
202. El avance de este for divide entre 2: después de 1 viene 1 / 2, que en enteros es
0, y ahí termina el ciclo.
weight |
n antes |
¿n >= weight? |
Escribe | n después |
|---|---|---|---|---|
| 128 | 202 | sí | 1 | 74 |
| 64 | 74 | sí | 1 | 10 |
| 32 | 10 | no | 0 | 10 |
| 16 | 10 | no | 0 | 10 |
| 8 | 10 | sí | 1 | 2 |
| 4 | 2 | no | 0 | 2 |
| 2 | 2 | sí | 1 | 0 |
| 1 | 0 | no | 0 | 0 |
$ echo 202 | build/to_binary
11001010
$ echo 300 | build/to_binary
Número fuera de rango
Error común. Un
;justo después del paréntesis termina elfor: su cuerpo queda vacío. El bloque de abajo se ejecuta una sola vez, al final. Aquí, congccavisando, se ve un solo número, y es el valor con el que se salió del ciclo:$ gcc -Wall -Wextra -std=c17 errors/semicolon_for.c -o build/semicolon_for errors/semicolon_for.c: In function ‘main’: errors/semicolon_for.c:7:5: warning: this ‘for’ clause does not guard... [-Wmisleading-indentation] 7 | for (i = 1; i <= 3; i++); | ^~~ errors/semicolon_for.c:8:5: note: ...this statement, but the latter is misleadingly indented as if it were guarded by the ‘for’ 8 | { | ^ $ build/semicolon_for 4Con
while (n > 0);es peor: sines positivo, el programa se queda preguntando para siempre, sin escribir nada.
Salirse por uno
El error más común con ciclos es dar una vuelta de más o una de menos. Se llama salirse por uno (off-by-one). Este programa quería escribir del 1 al 5:
for (int i = 1; i < 5; i++) {
$ gcc -Wall -Wextra -std=c17 errors/off_by_one.c -o build/off_by_one
$ build/off_by_one
1
2
3
4
gcc no avisa: i < 5 es C válido, solo que no es lo que se quería. La forma de
evitarlo es revisar a mano la primera vuelta y la última vuelta, sin ejecutar nada.
| Ciclo | Valores de i |
Vueltas (con n ≥ 1) |
|---|---|---|
for (int i = 0; i < n; i++) |
0 a n - 1 | n |
for (int i = 1; i <= n; i++) |
1 a n | n |
for (int i = 0; i <= n; i++) |
0 a n | n + 1 |
for (int i = 1; i < n; i++) |
1 a n - 1 | n - 1 |
Para contar, sirve esta regla. De a hasta b, incluyendo los dos, avanzando de 1 en
1, hay b - a + 1 valores. Del 3 al 7 hay 5 números, no 4. Los dos primeros ciclos de la
tabla dan n vueltas y son las formas que conviene usar. i < n empezando en 0 será la
forma habitual con arreglos, más adelante.
Idea clave. Prueben un ciclo con los bordes: una entrada que da cero vueltas, una que da una sola, y una normal. Es la misma idea de los bordes de la semana pasada.
Mejor < que !=
Con un paso distinto de 1, != es peligroso. En for (i = 0; i != 10; i += 3), i toma
los valores 0, 3, 6, 9, 12, y salta el 10. La condición nunca se hace falsa e i sigue
creciendo hasta el overflow, que es comportamiento indefinido. Con i < 10, el ciclo
termina aunque no caiga justo en 10.
No controlen un ciclo con double
Esto es más traicionero. En la semana 3 vieron que 0.1 no se puede guardar exactamente. Este programa quiere avanzar de 0.1 en 0.1 hasta llegar a 1:
for (double x = 0.0; x != 1.0; x += 0.1) {
Pide 17 decimales con %.17f, para ver lo que de verdad hay guardado. Esta salida es
la misma en toda máquina que use el formato habitual de double, el de la semana 3.
$ gcc -Wall -Wextra -std=c17 errors/double_never_equal.c -o build/double_never_equal
$ build/double_never_equal | head -n 12
0.00000000000000000
0.10000000000000001
0.20000000000000001
0.30000000000000004
0.40000000000000002
0.50000000000000000
0.59999999999999998
0.69999999999999996
0.79999999999999993
0.89999999999999991
0.99999999999999989
1.09999999999999987
Después de diez sumas, x no es 1.0 sino un poco menos, y la siguiente suma ya se pasó.
La condición x != 1.0 nunca se hace falsa. El programa no termina solo; aquí lo
detiene head.
Cambiar != por < hace que termine, pero tampoco es lo que queríamos. Como 0.99999…
es menor que 1.0, hay una vuelta de más:
$ gcc -Wall -Wextra -std=c17 errors/double_extra_turn.c -o build/double_extra_turn
$ build/double_extra_turn
Vueltas: 11
Lo correcto es contar con un entero y calcular el double en cada vuelta, como en
double_steps.c. La cantidad de vueltas no depende de ningún redondeo:
for (int i = 0; i <= 10; i++) {
double x = i / 10.0;
printf("%.1f\n", x);
}
$ build/double_steps | head -n 2
0.0
0.1
do while
while pregunta antes de entrar. do while pregunta después del cuerpo: por eso el
cuerpo se ejecuta al menos una vez. La forma es esta, y lleva ; al final:
do {
cuerpo
} while (condición);
El programa while_vs_do.c lo muestra con n en 5. Las dos condiciones dicen n < 5,
falsa desde el inicio:
int n = 5;
while (n < 5) {
printf("while: entré\n");
n++;
}
do {
printf("do while: entré\n");
n++;
} while (n < 5);
$ build/while_vs_do
do while: entré
n vale 6
Validar una entrada
El uso típico es pedir un dato hasta que sea válido, porque primero hay que leer y recién
después se puede preguntar. Por fin podemos pedirlo de nuevo, algo que el programa
read_in_range.c de la semana pasada no podía hacer.
do while es una herramienta de C. En pseudocódigo escribimos lo mismo con mientras, y
se nota que la lectura aparece dos veces:
algoritmo pedir_mes()
leer mes
mientras mes < 1 o mes > 12 hacer
escribir "Mes fuera de rango, escriba otro"
leer mes
fin mientras
escribir mes
fin algoritmo
En read_month.c, con do while, la lectura aparece una sola vez:
do {
if (scanf("%d", &month) != 1) {
printf("Entrada inválida\n");
return 1;
}
if (month < 1 || month > 12) {
printf("Mes fuera de rango, escriba otro\n");
}
} while (month < 1 || month > 12);
Fíjense en el primer if. scanf puede fallar en cada vuelta, así que se revisa dentro
del ciclo. Hay dos fallas distintas, y en las dos el programa termina:
$ echo 13 0 7 | build/read_month
Mes fuera de rango, escriba otro
Mes fuera de rango, escriba otro
Mes elegido: 7
$ echo abc | build/read_month; echo $?
Entrada inválida
1
$ echo 13 | build/read_month; echo $?
Mes fuera de rango, escriba otro
Entrada inválida
1
En el último caso, la entrada se acabó sin un mes válido. Sigamos el primer caso.
| Vuelta | scanf lee |
¿Devuelve 1? | month |
¿Fuera de rango? |
|---|---|---|---|---|
| 1 | 13 | sí | 13 | sí, se repite |
| 2 | 0 | sí | 0 | sí, se repite |
| 3 | 7 | sí | 7 | no, se sale |
Sin revisar scanf, no termina
Si no revisamos scanf, un dato malo produce un ciclo infinito. %d no puede leer abc:
no lo consume, lo deja donde está y devuelve 0. La siguiente vuelta encuentra lo mismo,
falla igual, y así para siempre. Cuando se acaba la entrada pasa lo mismo: scanf sigue
devolviendo EOF. Este programa, errors/read_forever.c, empieza con month en 0 y
nunca lo cambia:
do {
scanf("%d", &month);
printf("Mes fuera de rango\n");
} while (month < 1 || month > 12);
$ gcc -Wall -Wextra -std=c17 errors/read_forever.c -o build/read_forever
$ echo abc | build/read_forever | head -n 3
Mes fuera de rango
Mes fuera de rango
Mes fuera de rango
Error común. Un
scanfdentro de un ciclo sin revisar es un ciclo infinito esperando un dato malo. El ciclo debe terminar siscanfno devuelve lo pedido. Más adelante, en un ejercicio, verán cómo descartar el dato malo para volver a preguntar.
Error común. Una variable declarada dentro del bloque del
doya no existe en la condición, porque la condición está fuera de las llaves. Declárenla antes deldo.$ gcc -Wall -Wextra -std=c17 errors/do_scope.c -o build/do_scope errors/do_scope.c: In function ‘main’: errors/do_scope.c:10:14: error: ‘month’ undeclared (first use in this function) 10 | } while (month < 1 || month > 12); | ^~~~~ errors/do_scope.c:10:14: note: each undeclared identifier is reported only once for each function it appears in
¿Cuál ciclo elegir?
Cualquiera de los tres se puede escribir con otro, así que nunca es obligatorio uno en particular. Se elige el que deja el programa más claro.
| Situación | Conviene | Ejemplo |
|---|---|---|
| Se conoce la cantidad de vueltas antes de empezar | for |
la tabla del 1 al 12 |
| Se repite mientras algo se cumpla, y puede no dar ninguna vuelta | while |
las monedas de un vuelto |
| El cuerpo tiene que correr antes de poder preguntar | do while |
pedir un dato hasta que sea válido |
En C. Existen dos instrucciones más.
break, que vieron enswitch, también sale de un ciclo.continuesalta a la siguiente vuelta. (Concontinue, unfory suwhileequivalente se comportan distinto, porque elwhilese saltaría el avance.) En este curso no las necesitamos: preferimos que la condición del ciclo diga cuándo se termina. Si unswitchestá dentro de un ciclo, unbreaken elswitchsale solo delswitch.
Cuatro patrones
Casi todos los ciclos de un primer curso son combinaciones de cuatro patrones. Conviene reconocerlos, porque cada uno tiene su propio error típico.
Contador
Un contador es una variable que empieza en 0 y suma 1 cada vez que pasa algo. Aquí contamos cuántas notas aprueban, con 11 o más.
algoritmo contar_aprobados()
total ← 0
aprobados ← 0
mientras queden datos hacer
leer nota
total ← total + 1
si nota ≥ 11 entonces
aprobados ← aprobados + 1
fin si
fin mientras
escribir aprobados, total
fin algoritmo
¿Cómo sabe C que quedan datos? Por lo que devuelve scanf. Pero hay que leer antes de
preguntar, así que se lee una vez antes del ciclo y otra vez al final de cada vuelta. En
status queda lo último que devolvió scanf. Así es count_passed.c:
int status = scanf("%d", &grade);
while (status == 1) {
total++;
if (grade >= 11) {
passed++;
}
status = scanf("%d", &grade);
}
if (status != EOF) {
printf("Entrada inválida\n");
return 1;
}
El if de después del ciclo distingue las dos formas de salir. Si status es EOF, la
entrada se acabó, que es lo normal. Si no, scanf encontró algo que no es un número.
Recuerden de la semana 2 que EOF es un valor negativo, y que en la terminal se indica el
fin de la entrada con Ctrl+D. Aquí la entrada viene del archivo grades.txt, con <, como
en la semana 2; contiene 14, 9, 11, 20 y 10, uno por línea.
$ build/count_passed < grades.txt
Aprobaron 3 de 5
$ echo 14 abc | build/count_passed; echo $?
Entrada inválida
1
Error común. Escribir
while (scanf("%d", &n) != EOF). Parece correcto, pero conabcscanfdevuelve 0, que es distinto deEOF. El ciclo sigue, el dato malo sigue ahí, y no termina nunca. Pregunten siempre si devolvió lo que se pidió,== 1.$ gcc -Wall -Wextra -std=c17 errors/not_eof.c -o build/not_eof $ echo abc | build/not_eof | head -n 3 Llevo 1 datos; el último es 0 Llevo 2 datos; el último es 0 Llevo 3 datos; el último es 0
Acumulador
Un acumulador es una variable que va juntando un resultado. Ya vieron uno: sum en
sum_to_n.c. Su valor inicial es el resultado que corresponde a “ningún dato todavía”: 0
para una suma, 1 para un producto.
Error común. Olvidar darle el valor inicial:
int sum;y luegosum += x. Leer una variable sin valor es comportamiento indefinido, como vieron en la semana 2. El programa puede parecer correcto en una corrida y fallar en otra. Otro error frecuente es ponersum = 0dentro del ciclo, con lo que la suma se reinicia en cada vuelta.
Centinela
A veces no se sabe cuántos datos vienen. Una forma de avisar que se acabaron es un valor especial, que no es un dato: el centinela (sentinel). Imaginen la caja de una tienda. Se marcan precios en céntimos, y un 0 indica que la compra terminó.
algoritmo total_de_compra()
total ← 0
productos ← 0
leer precio
mientras precio ≠ 0 hacer
total ← total + precio
productos ← productos + 1
leer precio
fin mientras
escribir productos, total
fin algoritmo
Lean el orden con cuidado: se lee, se pregunta, se procesa y se vuelve a leer. Así, el 0
nunca se suma ni se cuenta. En sentinel_sum.c hay además que atender la falla de
scanf:
int status = scanf("%d", &price);
while (status == 1 && price != 0) {
total += price;
items++;
status = scanf("%d", &price);
}
if (status != 1) {
printf("Entrada inválida\n");
return 1;
}
La condición usa el cortocircuito de la semana pasada: si status no es 1, price no tiene
un dato nuevo y no se llega a mirar. Hay tres formas de terminar: llega el 0, la entrada
se acaba o hay un dato inválido.
scanf lee |
status |
price |
¿Sigue? | total |
items |
|---|---|---|---|---|---|
| 1250 | 1 | 1250 | sí | 1250 | 1 |
| 300 | 1 | 300 | sí | 1550 | 2 |
| 4050 | 1 | 4050 | sí | 5600 | 3 |
| 0 | 1 | 0 | no | 5600 | 3 |
$ echo 1250 300 4050 0 | build/sentinel_sum
3 productos: 56.00 soles
$ echo 0 | build/sentinel_sum
0 productos: 0.00 soles
$ echo 1250 300 | build/sentinel_sum; echo $?
Entrada inválida
1
El centinela debe ser un valor que nunca sea un dato real. El 0 sirve para precios, pero no serviría para temperaturas.
Buscar el mayor
Para buscar el mayor, se guarda el mayor visto hasta ahora y se compara cada dato nuevo con él. La duda es con qué valor empieza. Respuesta: con el primer dato.
algoritmo mayor_de_n()
leer n
para i desde 0 hasta n - 1 hacer
leer temperatura
si i = 0 o temperatura > mayor entonces
mayor ← temperatura
fin si
fin para
escribir mayor
fin algoritmo
En max_of_n.c, el programa lee primero cuántas temperaturas vienen:
int max = 0; // Este 0 nunca se usa: la primera vuelta lo reemplaza.
for (int i = 0; i < n; i++) {
int temperature;
if (scanf("%d", &temperature) != 1) {
printf("Entrada inválida\n");
return 1;
}
if (i == 0 || temperature > max) {
max = temperature;
}
}
En el pseudocódigo, como en C, cuando i = 0 ya es cierto no se mira mayor, que todavía no
tiene valor. temperature se declara dentro del bloque, así que existe solo durante cada vuelta y no
conserva su valor de una vuelta a la siguiente. i == 0 || es otra vez el cortocircuito: en
la primera vuelta es verdadero sin comparar nada. Sigamos -3 -7 -1.
i |
temperature |
¿i == 0? |
¿temperature > max? |
max |
|---|---|---|---|---|
| 0 | -3 | sí | no se evalúa | -3 |
| 1 | -7 | no | no | -3 |
| 2 | -1 | no | sí | -1 |
$ echo 3 -3 -7 -1 | build/max_of_n
La mayor es -1
$ echo 3 8 -7 12 | build/max_of_n
La mayor es 12
Error común. Empezar el mayor en 0. Con datos positivos parece funcionar, pero 0 no es un dato. Con temperaturas de una noche de helada, el programa responde algo que nadie midió:
$ gcc -Wall -Wextra -std=c17 errors/max_from_zero.c -o build/max_from_zero $ echo 3 -3 -7 -1 | build/max_from_zero La mayor es 0
Leer hasta el fin de la entrada con getchar
scanf lee datos con un formato. A veces queremos algo más simple: mirar la entrada byte
por byte, sea lo que sea. Para eso está getchar(), de stdio.h. Cada llamada devuelve el
siguiente byte de la entrada, incluidos los espacios y los saltos de línea. Cuando ya no
quedan, devuelve EOF.
El patrón es el de siempre: leer antes de preguntar, y volver a leer al final de la vuelta.
algoritmo contar_bytes()
cuenta ← 0
mientras queden bytes hacer
leer c
cuenta ← cuenta + 1
fin mientras
escribir cuenta
fin algoritmo
En count_bytes.c:
int count = 0;
int c = getchar();
while (c != EOF) {
count++;
c = getchar();
}
El archivo cities.txt tiene tres líneas: Puno, Cusco y Piura. Son 14 letras y 3 saltos
de línea. wc -c, de la semana 3, cuenta lo mismo.
$ build/count_bytes < cities.txt
17 bytes
$ wc -c cities.txt
17 cities.txt
$ echo ñ | build/count_bytes
3 bytes
La última línea no es un error. Recuerden que en UTF-8 la ñ ocupa 2 bytes, y echo
agrega un salto de línea. getchar cuenta bytes, no letras.
Sin <, el programa lee del teclado y espera. Se termina la entrada con Ctrl+D al inicio
de una línea, como en la semana 2.
Procesar lo que se lee
Junto a getchar está putchar(c), que escribe un solo byte. Con los dos podemos transformar
un texto. to_upper.c cambia las minúsculas por mayúsculas:
int c = getchar();
while (c != EOF) {
if (c >= 'a' && c <= 'z') {
c = c - 'a' + 'A';
}
putchar(c);
c = getchar();
}
c - 'a' es la posición de la letra en el alfabeto ASCII, y al sumarle 'A' se obtiene la
mayúscula. Esto funciona porque en ASCII las letras están seguidas. Y solo sirve para ASCII:
$ echo hola mundo | build/to_upper
HOLA MUNDO
$ echo ñandú | build/to_upper
ñANDú
Los bytes de ñ y de ú no están en el rango de 'a' a 'z', así que pasan sin cambios.
La forma corta, y dos trampas
Verán este ciclo en casi todo programa de C. Es el mismo patrón, en una línea. Así es
copy.c, que copia la entrada en la salida:
while ((c = getchar()) != EOF) {
putchar(c);
}
¿No dijimos la semana pasada que asignar dentro de una condición era el error clásico? Aquí es
a propósito. Se lee de adentro hacia afuera: getchar() trae un byte, c = ... lo guarda, y el
valor de esa asignación se compara con EOF. La condición es una comparación completa, así que
gcc no avisa.
$ build/copy < cities.txt
Puno
Cusco
Piura
Los paréntesis interiores son indispensables. Como = tiene la menor precedencia, sin ellos
primero se compara, y c recibe el resultado de la comparación, que es 1, y no el byte. Por eso
este programa nunca ve un salto de línea:
$ gcc -Wall -Wextra -std=c17 errors/getchar_no_parens.c -o build/getchar_no_parens
errors/getchar_no_parens.c: In function ‘main’:
errors/getchar_no_parens.c:8:12: warning: suggest parentheses around assignment used as truth value [-Wparentheses]
8 | while (c = getchar() != EOF) {
| ^
$ build/getchar_no_parens < cities.txt
0 saltos de línea
La segunda trampa es el tipo. getchar devuelve un int, y c debe ser int, nunca
char. La razón es que tiene que poder devolver los 256 valores posibles de un byte y además
EOF, y eso no cabe en un char. Lo que pasa con char c depende de la máquina, porque el
estándar no dice si char tiene signo, como vieron en la semana 3:
- Donde
charno tiene signo, como en Linux sobre ARM,cnunca puede ser negativo, así que nunca es igual aEOFy el ciclo no termina. Con-Wextra,gcclo avisa allí concomparison is always true due to limited range of data type. - Donde
chartiene signo, como en Linux sobre x86-64, el programa parece funcionar ygccno avisa. Pero un byte 0xFF en la entrada se confunde conEOF, y la lectura se detiene antes de tiempo.
Razonar antes de ejecutar
Antes de correr un ciclo, repasen esta lista. Toma un minuto, y evita la mayoría de los errores de este capítulo.
| Pregunta | Qué revisar |
|---|---|
| ¿Inicio? | Toda variable que se lee en la condición o en el cuerpo tiene valor antes de entrar |
| ¿Parada? | La condición llega a ser falsa, también con entradas inválidas o si la entrada se acaba |
| ¿Avance? | Alguna línea del cuerpo acerca la condición a ser falsa |
| ¿Primera vuelta? | Los valores son los que se querían |
| ¿Última vuelta? | Ni una de más ni una de menos |
| ¿Cero vueltas? | El resultado sigue teniendo sentido. Por ejemplo, no se divide entre 0 |
| ¿Puede dar overflow? | El acumulador cabe en su tipo para los datos permitidos |
El laboratorio, paso a paso
El laboratorio tiene cuatro programas. Los archivos de partida están en
code/week-05/lab. En cada uno, el comentario inicial
dice exactamente qué debe escribir. make test-lab los comprueba, y falla hasta que terminen.
Si un programa de ustedes se queda en un ciclo infinito, la prueba lo corta y falla. Aquí están
las ideas, no el código.
Suma, promedio y mayor hasta un 0
Es un centinela, con un acumulador, un contador y la búsqueda del mayor, todo en el mismo ciclo.
Tomen como base sentinel_sum.c. Tres cuidados:
- Sin datos. Si el primer número es 0, el contador queda en 0, y el promedio sería una
división entre 0. El enunciado pide
Sin datos. - El promedio. Es
(double) sum / count, con%.2f, como en la semana 2. Sin el cast, sería división entera. - El mayor. Puede haber solo negativos. Si usan el contador para saber que es el primer dato, actualicen el contador después de esa comparación.
Prueben con 10 15 5 0, con 0, con -5 -2 -9 0, con 4 abc y con 4 5 (sin centinela).
¿Es primo?
Un primo es un entero mayor que 1 que solo se divide exactamente entre 1 y entre sí mismo. Se
prueban divisores desde 2 y se deja de buscar en cuanto aparece uno. Para eso, usen una variable
que recuerde la respuesta (prime), que empieza en “sí” y cambia a “no” si aparece un divisor, y
pongan esa variable en la condición del ciclo. Decidan aparte lo que pasa con 0, 1 y los negativos,
que no son primos. El 2 sí lo es, y no entra a ninguna vuelta.
No hace falta llegar hasta n. Si n es a * b, uno de los dos factores es a lo sumo la raíz
cuadrada de n. Por lo tanto, basta probar d mientras d * d no pase de n. Pero con cuidado:
el enunciado incluye 2147483647, el mayor int, y ahí d * d da overflow cuando d llega a
46341. El sanitizer se los va a mostrar. Escriban la condición sin multiplicar: d <= n / d dice
lo mismo con enteros positivos, y no hace overflow.
Prueben 0, 1, 2, 3, 4, 9, 25, 49, 97, -7 y 2147483647. Los cuadrados perfectos son el borde: si
la condición fuera d < n / d, 49 saldría como primo.
Contar líneas
Se cuenta cada '\n' que entrega getchar, hasta EOF. El archivo viene de la entrada, con <.
Hay tres archivos para probar en lab/. El comando wc -l también cuenta saltos de línea:
$ wc -l lab/three_lines.txt
3 lab/three_lines.txt
$ wc -l lab/no_final_newline.txt
2 lab/no_final_newline.txt
$ wc -l lab/empty.txt
0 lab/empty.txt
no_final_newline.txt tiene tres renglones, pero el último no termina en salto de línea. El
enunciado pide contar igual que wc -l, así que da 2. Recuerden que c debe ser int.
Validar una nota
Es read_month.c con otros límites y otros mensajes. Usen do while, revisen scanf dentro del
ciclo y terminen si falla. El programa no escribe ningún mensaje antes de leer, porque sus pruebas
comparan la salida exacta. Prueben con 25 -1 20, con 0, con abc y con 25 seguido del fin de
la entrada.
Resumen
- Un ciclo repite un bloque. Se entiende con tres preguntas: inicio, parada y avance.
whilepregunta antes de cada vuelta y puede dar cero vueltas.for (inicio; condición; avance)se comporta como unwhilecon esas tres partes.avancecorre después del cuerpo. Al salir, el contador tiene el primer valor que hizo falsa la condición.do whilepregunta después del cuerpo, que corre al menos una vez. Sirve para validar una entrada.para i desde a hasta bincluyeb: se traduce con<=. Deaabhayb - a + 1valores.- Salirse por uno: revisen la primera vuelta, la última y el caso de cero vueltas.
i++,i--,+=,-=y/=son formas cortas de asignar.- Un
scanfdentro de un ciclo se revisa dentro del ciclo. La entrada inválida y el fin de la entrada deben terminar el ciclo. Compárenlo con== 1, no con!= EOF. - Patrones: contador (empieza en 0), acumulador (empieza en el resultado de “ningún dato”), centinela (no se procesa) y mayor (empieza en el primer dato).
- Un acumulador
intque se pasa del límite es comportamiento indefinido. - No controlen un ciclo con un
double: cuenten con un entero. getchardevuelve unint, con el siguiente byte oEOF. Guárdenlo en unint. Cuenta bytes, no letras.
Ejercicios
Los ejercicios no necesitan arreglos ni funciones propias. Todo programa revisa su scanf.
-
Sigan a mano este ciclo con una tabla, y luego compruébenlo con un programa. ¿Qué valores toma
i? ¿Cuántas vueltas da? ¿Cuánto valeial salir?int i; for (i = 10; i > 0; i -= 3) { printf("%d\n", i); } -
¿Cuántas líneas escribe cada fragmento? Respondan sin compilar y luego compruébenlo.
a) for (int i = 0; i <= 5; i++) { printf("x\n"); } b) for (int i = 1; i < 5; i++) { printf("x\n"); } c) for (int i = 5; i < 5; i++) { printf("x\n"); } d) int i = 5; do { printf("x\n"); i++; } while (i < 5); -
Reescriban
for (int i = 5; i >= 1; i--) { printf("%d\n", i); }conwhiley condo while. Luego cambien el valor inicial deia 0 en las tres versiones. ¿Cuál se comporta distinto, y por qué? -
Escriban en pseudocódigo y en C un programa que lea
n, entre 1 y 12, y escriban!, el producto de los enteros de 1 an. Dibujen su diagrama de flujo en una hoja. ¿Con qué valor empieza el acumulador? Después quiten el límite y prueben con 13. ¿Qué dice el sanitizer? ¿Por qué no sirve el número que escribe? -
Escriban un programa que lea un entero no negativo y escriba la suma de sus dígitos: con 1234 escribe 10. Usen
% 10y/ 10. Sigan a mano el caso 1234 con una tabla. ¿Qué escribe con 0? -
Lean notas hasta el centinela -1, sin procesarlo. Escriban cuántos aprobaron (11 o más) y cuántos desaprobaron. Si un dato no es un número, si está fuera de 0 a 20, o si la entrada se acaba antes del -1, el programa avisa y termina con
return 1. ¿Por qué aquí el 0 no serviría de centinela? -
Modifiquen
max_of_n.cpara que escriba también la menor temperatura y la diferencia entre la mayor y la menor. Pruébenlo con una sola temperatura y con todas negativas. -
Escriban un menú con
do while. El programa lee una opción conscanfy la atiende conswitch: 1 escribeHola, 2 escribeChau, y 0 termina. Otra opción escribeOpción inváliday vuelve a leer. Siscanffalla, avisa y termina conreturn 1. Pruébenlo conecho 1 9 2 0 | .... -
Con
getchar, cuenten cuántos dígitos de'0'a'9'y cuántas minúsculas ASCII tiene la entrada. Pruébenlo conecho año 2026 | .... ¿Por qué lañno cuenta como minúscula? -
Mejoren el laboratorio de contar líneas para que también cuente un último renglón que no termina en
\n. Pista: guarden el byte anterior en otra variableint. El resultado debe ser 3 conlab/no_final_newline.txt, 3 conlab/three_lines.txty 0 conlab/empty.txt. -
Modifiquen
read_month.cpara que, ante un dato que no es un número, vuelva a preguntar en vez de terminar. Siscanfdevuelve 0, descarten congetchartodo hasta el salto de línea, o hastaEOF. SiscanfdevuelveEOF, terminen conreturn 1. Inicienmonthen 0, porque si el primerscanffalla, la condición deldo whileleería una variable sin valor. Comprueben con el comando de abajo que la última línea esMes elegido: 7. ¿Qué pasaría sin descartar? ¿Y si el descarte no se detuviera enEOF?printf 'abc\n13\n7\n' | build/read_month_retry(
printftambién es un comando de la terminal:\nes el salto de línea, como en C.) -
Sucesión de Collatz. Lean un entero
nde 1 a 1000. Mientrasnno sea 1: si es par, divídanlo entre 2; si es impar, cámbienlo por3 * n + 1. Escriban cuántos pasos hizo. Con 27 son 111. Sigan a mano el caso 6. ¿nbaja en cada vuelta? Hasta donde se sabe, nadie ha demostrado que este ciclo termine para todo entero positivo. -
Reto.
adder4.cde la semana pasada repite la cuenta por cada columna. Reescríbanlo con unforde cuatro vueltas. Sin arreglos, lean los dos números con%d, de modo que0101se lee como el entero 101, y saquen cada bit con% 10y/ 10, de derecha a izquierda. Vayan armando la suma con un peso que se multiplica por 10 en cada vuelta, y escríbanla con%04d, que rellena con ceros hasta 4 cifras. Con0101 0011debe escribir1000y con1000 1000,0000y un acarreo final de 1.