59 KiB
Capítulo 14 — Entrada y salida estándar (sin archivos)
Capítulo intermedio, pero escrito para que se entienda de verdad. Se apoya en variables (
02), operadores (03),ify bucles (04), funciones (05) y arreglos y cadenas (06).Es el puente hacia el capítulo de archivos (
15-archivos). Todo lo que aprendas acá sobrestdin, búferes yEOFlo vas a reusar tal cual para leer archivos. La única diferencia será que, en vez de leer del teclado, vas a leer de un archivo.
Cómo leer este capítulo
Bienvenido al capítulo que separa a los que "hacen que ande" de los que entienden qué pasa. Acá no vas a memorizar tablas: vas a mirar, probar y romper cosas hasta que los conceptos te queden pegados.
Está dividido en ocho partes. Después de cada idea importante hay una práctica —uno o varios ejercicios cortos— para que la uses ya mismo, antes de avanzar. No saltees las prácticas: son la mitad del capítulo.
| Parte | De qué habla | La idea en una frase |
|---|---|---|
| 1 | Los tres flujos | Tu programa tiene tres tuberías: una de entrada y dos de salida. |
| 2 | Leer byte a byte | getchar saca de a uno, y EOF avisa cuándo se acabó. |
| 3 | Leer líneas | fgets saca una línea entera sin desbordarse. |
| 4 | El búfer | Por qué un Enter cambia todo, y por qué scanf deja basura. |
| 5 | Leer con formato | scanf y su mini-lenguaje de formatos. |
| 6 | Caracteres | Por qué una á pesa dos bytes. |
| 7 | Robustez | Que tu programa no se rompa nunca. |
| 8 | Proyecto final | Construís tu propia consola de comandos. |
Los ejercicios se numeran en orden: 14.1, 14.2, 14.3, y así. Cada uno trae su escenario de
prueba (qué entra y qué tiene que salir) para que puedas comprobar tu solución.
Regla de oro del libro: el capítulo explica y muestra cómo funciona cada cosa, pero los ejercicios no traen la solución. El enunciado te da el problema; la respuesta la escribís vos.
PARTE 1 — Tu programa tiene tres tuberías
Antes de tocar una sola función, entendamos el terreno. Es más simple de lo que parece.
1.1 El día que descubrí que mi programa no sabe de dónde lee
Imaginate que tu programa es una fábrica. La fábrica no sabe si la materia prima llega en camión, en tren o por un caño: solo sabe que algo entra por una entrada, se procesa, y algo sale por las salidas. En C, esa entrada y esas salidas son tuberías (en inglés, streams, "flujos").
Todo programa en C nace con tres tuberías ya conectadas:
(de dónde vengan los datos)
│
▼
┌───────────┐
│ stdin │ ← ENTRADA ESTÁNDAR (por acá entran los datos)
└─────┬─────┘
│
▼
╔══════════════╗
║ TU PROGRAMA ║
╚══╤═══════╤═══╝
│ │
┌──────▼──┐ ┌─▼────────┐
│ stdout │ │ stderr │ ← DOS salidas
└────┬────┘ └────┬─────┘
│ │
▼ ▼
(salida normal) (errores)
stdines la tubería de entrada. Por ahí entran los datos.stdoutes la tubería de salida normal. Por ahí sale el resultado del programa.stderres la tubería de errores. Por ahí salen los mensajes de problema.
Esas tres tienen un nombre con st adelante porque viene de standard: estándar. No las abrís vos.
Existen desde que arranca main, como la luz de tu casa: no la instalás, ya está.
1.2 FILE *: la ficha de la tubería
Cada tubería, en el código, se maneja con algo que se llama FILE *. Podés pensarlo como la
factura o la ficha de esa tubería: un papelito que dice "esta es la tubería de entrada, así
está configurada, así se va leyendo". No es la tubería en sí: es su identificación.
#include <stdio.h>
int main(void) {
/* stdin, stdout y stderr ya vienen declarados en <stdio.h>.
Son de tipo FILE *. No hay que abrirlos ni cerrarlos. */
(void)stdout;
return 0;
}
¿Por qué
FILE *y no simplemente "el teclado"? Porque tu programa no habla con el teclado: habla con la tubería. Eso es lo que lo hace genial. La mismastdinpuede estar conectada al teclado, a un archivo o a otro programa, y tu código no cambia ni una línea. Eso lo vas a ver ya mismo, en la parte más divertida.
1.3 El truco más lindo de la consola: la misma tubería, conectada a cualquier cosa
Acá está la magia. Vos escribís un programa, y el shell (la terminal, la ventanita donde escribís comandos) decide a qué conecta sus tuberías. Mirá cómo cambia todo sin tocar el programa.
Vamos a usar cat, un programa clásico que simplemente copia lo que entra a lo que sale. Es como un
espejo: lo que le des, te lo devuelve.
Forma 1: entrada desde el teclado. El shell conecta la stdin de cat a tu teclado.
$ cat
hola mundo ← lo que escribís vos
hola mundo ← lo que devuelve cat (el eco)
esto es una prueba
esto es una prueba
(Ctrl+Z y Enter en Windows / Ctrl+D en Linux y macOS para terminar)
Forma 2: entrada desde una tubería. Con el símbolo | (la "tubería"), conectás la salida de
un programa a la entrada de otro. Fijate que echo escribe y cat lee, y ninguno de los
dos sabe del otro.
$ echo "hola mundo" | cat
hola mundo
Forma 3: entrada desde un archivo. Con el símbolo <, conectás la stdin de cat a un archivo.
$ cat < datos.txt
primera línea del archivo
segunda línea del archivo
Momento "ajá". En los tres casos,
cathizo lo mismo internamente: leer destdiny escribir enstdout. Nunca supo si leía del teclado, de otro programa o de un archivo. El shell cambió a qué estaba conectada la tubería, nada más.
1.4 Y las salidas también se pueden desviar
Igual que la entrada, podés desviar las salidas. El símbolo > manda la salida (normal) a un
archivo en vez de la pantalla:
$ cat < datos.txt > copia.txt # leo de datos.txt y escribo en copia.txt
$ cat copia.txt
primera línea del archivo
segunda línea del archivo
Ahora, la parte que casi nadie sabe al principio: > desvía stdout, pero NO stderr. Los
errores siguen saliendo por su propio canal. Probémoslo pidiendo un archivo que no existe:
$ ls archivo_que_no_existe
ls: cannot access 'archivo_que_no_existe': No such file or directory
$ ls archivo_que_no_existe > salida.txt
ls: cannot access 'archivo_que_no_existe': No such file or directory ← ¡sigue en pantalla!
$ cat salida.txt
← salida.txt está VACÍO: eso fue un error
El mensaje de error no fue a salida.txt, porque pertenece a stderr. Eso es exactamente para
lo que existe stderr: así podés guardar la salida útil en un archivo sin perder los errores de
vista.
| Símbolo | Qué desvía | Ejemplo |
|---|---|---|
< |
La entrada (stdin) desde un archivo |
prog < datos.txt |
> |
La salida normal (stdout) a un archivo |
prog > out.txt |
2> |
La salida de error (stderr) a un archivo |
prog 2> err.txt |
| |
La salida de uno a la entrada de otro | echo hola | prog |
2>&1 |
Manda stderr al mismo lugar que stdout |
prog > todo.txt 2>&1 |
Truco de laboratorio. Para ver la diferencia en vivo, corré cualquier comando tres veces:
cmd,cmd > out.txt 2> err.txt, ycmd > todo.txt 2>&1. Después mirá qué cayó en cada archivo. Es la mejor forma de sentir los canales separados en la mano.
1.5 Los tres flujos vistos desde el código
Desde C, los tres son FILE *, y los tres se pueden usar tanto para leer como para escribir (aunque
en la práctica stdin se lee y las otras dos se escriben). printf escribe en stdout;
fprintf(stderr, ...) escribe en stderr:
#include <stdio.h>
int main(void) {
/* fprintf: escribe con formato en un flujo cualquiera.
Primer argumento: el flujo (acá stdout, que es igual que usar printf).
El resto: el formato y los datos, exactamente como printf. */
fprintf(stdout, "Esto es la salida normal\n");
/* Mismo fprintf, pero apuntando a stderr: esto es un mensaje de error. */
fprintf(stderr, "Esto es un mensaje de error\n");
return 0;
}
Si lo corrés y desviás solo la salida normal, el mensaje de error queda en pantalla:
$ ./hola > out.txt
Esto es un mensaje de error ← quedó en pantalla (stderr)
$ cat out.txt
Esto es la salida normal ← esto sí fue al archivo
1.6 Un detalle que sorprende: el orden no siempre es el que escribiste
stderr normalmente no espera: envía cada mensaje al instante. stdout, en cambio, suele
acumular (guardar en un búfer) y enviar todo junto, sobre todo cuando la salida va a un archivo o a
un caño. Resultado: si mezclás escrituras a stdout y a stderr, el orden en pantalla puede no ser el
orden en que las programaste. Es un motivo más para no mezclar la salida normal con los errores.
1.7 "Estándar" y "multiplataforma": dos palabras que valen oro
Cuando decimos que getchar o fgets son estándar, queremos decir que están descritas en el
contrato del lenguaje C (el estándar ISO C). Ese contrato define el nombre, los parámetros y
qué hacen, y cualquier implementación que cumpla el estándar (GCC, Clang, MSVC…) las ofrece con
el mismo significado. Por eso el mismo código corre en Windows, Linux y macOS: eso es ser
multiplataforma.
Pero atención: el estándar garantiza el contrato, no los detalles de cada máquina. Mirá la diferencia:
| Garantizado por el estándar (vale en todos lados) | Depende del entorno (cambia según la máquina) |
|---|---|
Los nombres y parámetros de las funciones de <stdio.h> |
Qué tecla produce EOF: Ctrl+Z (Windows) o Ctrl+D (Linux/macOS) |
Cuántas asignaciones hizo scanf, qué devuelve fgets… |
Si el teclado muestra o no lo que escribís (el "eco") |
Que EOF es negativo, distinto de todo byte (0–255) |
La codificación de los caracteres (UTF-8, Latin-1, CP1252…) |
Que las cadenas terminan en '\0' |
El tamaño de los búferes y el momento en que se vuelcan |
Para guardar. Tu programa lee y escribe tuberías. El shell decide a qué está conectada cada una. Tu código escribe una vez y funciona en teclado, caño y archivo… siempre que respetes el contrato de cada función. Lo que cambia de máquina a máquina son los detalles, no el contrato.
Práctica de la Parte 1
Ejercicio 14.1 — Separar la salida útil de los avisos
Conceptos · stdout, stderr, fprintf, redirección de salidas
Contexto · Las herramientas de línea de comandos separan la salida útil (stdout) de los
diagnósticos (stderr), para que puedas guardar una sin perder los otros. Simplificación: un
programa que "cierra un trabajo" y tiene que avisar de un problema sin ensuciar el resultado.
Consigna · Escribí un programa que simule el cierre de un reporte: manda las líneas informativas
(dos o tres, por ejemplo "Iniciando reporte...", "Procesando 128 registros...", "Reporte generado
correctamente.") a stdout, y el aviso de problema (por ejemplo "Aviso: 3 registros tenían fecha
inválida.") a stderr. Nunca al revés.
Entrada · Ninguna: el programa no lee nada.
Salida · Las líneas informativas por stdout; el aviso por stderr.
Escenario de prueba
$ ./reporte > salida.txt 2> errores.txt
$ cat salida.txt
Iniciando reporte...
Procesando 128 registros...
Reporte generado correctamente.
$ cat errores.txt
Aviso: 3 registros tenían fecha inválida.
Criterios de aceptación
- Las líneas informativas salen solo por
stdout. - El aviso de problema sale solo por
stderr. - Al correr con
>y2>, cada mensaje cae en su archivo y ninguno aparece en el otro. - Usa
fprintfindicando el flujo (stdout/stderr) en cada escritura.
Ejercicio 14.2 — Detective de flujos
Conceptos · stdout, stderr, redirección (>, 2>, 2>&1), tuberías
Contexto · Antes de creer que entendiste los flujos, hay que verlos. Vas a conectar el mismo programa de varias maneras y comparar qué termina dónde.
Consigna · Tomá el programa de 14.1 (o escribí uno mínimo que mande una línea a stdout y otra a
stderr) y corrélo de estas formas, anotando para cada una qué queda en pantalla y qué en cada
archivo:
- sin redirección;
./programa > out.txt;./programa 2> err.txt;./programa > out.txt 2> err.txt;./programa > todo.txt 2>&1.
Al final, respondé: ¿cambió el programa? ¿cambió el resultado? ¿quién decidió a dónde va cada línea?
Entrada · Ninguna: el programa no lee nada.
Salida · Dos líneas (una por stdout, una por stderr), que caen en distintos destinos según cómo
las redirijas.
Escenario de prueba
$ ./dos > out.txt 2> err.txt
$ cat out.txt
salida normal
$ cat err.txt
mensaje de error
$ ./dos > todo.txt 2>&1
$ cat todo.txt
salida normal
mensaje de error
Criterios de aceptación
- El mismo binario se comporta distinto solo por cómo el shell conecta sus salidas (sin recompilar).
- Con
> out.txt 2> err.txt, cada mensaje cae en su archivo. - Con
2>&1, los dos mensajes terminan en el mismo archivo. - Sabés explicar qué cambió (la conexión) y qué no cambió (el programa).
PARTE 2 — Leer byte a byte: getchar y el famoso EOF
Ya sabemos de dónde vienen los datos. Ahora empecemos a sacarlos, de a poquito.
2.1 getchar: el caramelito de a uno
getchar() hace una sola cosa: saca un byte de stdin. Nada más. Cada vez que la llamás, te da el
próximo byte de la entrada. Como sacar caramelos de un frasco, de a uno por vez.
#include <stdio.h>
int main(void) {
/* getchar: lee UN byte de stdin y lo devuelve.
Lo guardamos en un int (¡no en un char!, ver 2.4). */
int c = getchar();
(void)c;
return 0;
}
Si tecleás hola y apretás Enter, no hay un byte esperando: hay cinco (h, o, l, a y
el '\n' del Enter). getchar te dará uno por llamada:
1ª llamada a getchar() → 'h'
2ª llamada a getchar() → 'o'
3ª llamada a getchar() → 'l'
4ª llamada a getchar() → 'a'
5ª llamada a getchar() → '\n' (el Enter que apretaste)
6ª llamada a getchar() → EOF (ya no hay más)
Lo mismo pasa con los números, y ahí está la trampa más grande de todas: para getchar, un número
no existe. Solo existen sus dígitos, uno por uno. Si tecleás 304 y apretás Enter, no hay un
"trescientos cuatro" esperando: hay cuatro bytes ('3', '0', '4' y el '\n').
Tecleás: 304 <Enter>
1ª llamada a getchar() → '3' (el byte del dígito tres)
2ª llamada a getchar() → '0' (el byte del dígito cero)
3ª llamada a getchar() → '4' (el byte del dígito cuatro)
4ª llamada a getchar() → '\n' (el Enter que apretaste)
5ª llamada a getchar() → EOF (ya no hay más)
Ojo con esto.
getcharte devuelve el byte del carácter, no el valor del número. Cuando lee'3', te da un byte (cuyo valor es51), no el número3. Armar el número304a partir de esos dígitos es un trabajo tuyo, no degetchar. Por eso, para leer números "de verdad", más adelante vas a usarscanf(Parte 5).
2.2 El patrón que vas a repetir mil veces
Casi todos los programas que leen byte a byte usan el mismo esqueleto: leer y procesar, en un bucle, hasta que se acabe la entrada.
#include <stdio.h>
int main(void) {
int c;
/* Bucle clásico: leo un byte; si no es EOF, lo proceso y repito.
getchar() devuelve EOF cuando ya no queda nada por leer. */
while ((c = getchar()) != EOF) {
putchar(c); /* putchar: escribe UN byte en stdout (el espejo de getchar) */
}
return 0;
}
Este programita es un espejo: copia la entrada a la salida. Es, básicamente, nuestro cat.
2.3 EOF: el timbre que dice "se acabó la fila"
EOF significa End Of File (fin de archivo), pero no lo interpretes como "fin de un archivo de
disco". Es más general: significa "no hay más datos para leer", venga de donde venga.
- Si leés de un archivo y llegaste al final →
EOF. - Si leés de una tubería y el otro programa terminó →
EOF. - Si leés del teclado y apretás Ctrl+Z + Enter (Windows) o Ctrl+D (Linux/macOS) →
EOF.
Pensalo como el timbre del recreo: no es un alumno más en la fila, es la señal de que la fila
se terminó. getchar devuelve ese timbre como un valor especial: EOF.
Byte 'h' → valor 104 (es un dato)
Byte '\n' → valor 10 (es un dato)
EOF → valor -1 (¡no es un dato! es "no hay más")
2.4 La trampa N.º 1: guardar getchar en un char
Fijate en algo raro: getchar no devuelve un char, devuelve un int. ¿Por qué?
Porque tiene que poder representar 256 bytes posibles (de 0 a 255) y además un valor extra:
EOF (-1). Un char no tiene lugar para ese "valor extra" sin pisar un byte real. Si guardás el
resultado en un char, pasan cosas feas:
- El byte
0xFF(que en algunos teclados/enviros es la letraÿ) puede convertirse en-1, ¡y-1esEOF! Entonces un dato real se confunde con "se acabó". - O al revés: comparás contra
EOFy la comparación nunca da verdadera, aunque la entrada terminó.
La regla, entonces: el resultado de getchar (y de getc/fgetc) se guarda siempre en un
int, y contra EOF se compara en ese int.
int c; /* int, NO char */
while ((c = getchar()) != EOF) {
/* acá c es un byte válido entre 0 y 255 */
}
2.5 getchar, getc y fgetc: tres nombres, casi lo mismo
Existen tres primos que leen un byte, y hacen lo mismo:
int getchar(void); /* lee SIEMPRE de stdin */
int getc(FILE *stream); /* lee de CUALQUIER flujo (ej.: getc(stdin)) */
int fgetc(FILE *stream); /* igual que getc, pero siempre es función */
getchar()es la versión fija astdin: comodísima para la consola.getc(stdin)yfgetc(stdin)hacen lo mismo quegetchar(), pero sirven para cualquier flujo (muy útil cuando llegues a los archivos).- La única diferencia fina:
getcpuede estar implementada como macro yfgetcsiempre es función. En la práctica, uságetcharpara la consola yfgetc(fp)para un archivo.
Práctica de la Parte 2
Ejercicio 14.3 — Contador de caracteres seguro
Conceptos · getchar, int vs char, EOF, bytes altos (0xFF)
Contexto · Contar bytes de la entrada es lo que hace la herramienta wc -c. Simplificación: contar
bytes leídos de stdin hasta EOF, sin confundir un byte 0xFF con el fin de archivo.
Consigna · Leer de stdin byte a byte con getchar() hasta EOF y contar cuántos bytes se
leyeron. La variable de lectura y el contador deben ser int.
Entrada · Cualquier secuencia de bytes (texto o no) hasta EOF.
Salida · Una línea con la cantidad de bytes leídos.
Escenario de prueba
printf 'hola' | ./contador
Caracteres leídos: 4
printf '\xff\xffh' | ./contador (dos bytes 0xFF y una 'h')
Caracteres leídos: 3
Para comprobar que lo hiciste bien: el conteo debe dar 3 aunque haya bytes
0xFF. Si guardaste en unchar, el programa corta en el primer0xFFy te da 0.
Criterios de aceptación
- Usa
intpara la lectura y para el contador. - Cuenta todos los bytes, incluidos
0x00y0xFF. - Termina limpio en
EOF, sin bucle infinito. - El resultado coincide con el tamaño real de la entrada.
Ejercicio 14.4 — ¿Cuántos saltos de línea hay?
Conceptos · getchar, comparación de bytes, '\n', EOF
Contexto · Los editores cuentan líneas contando fines de línea. Simplificación: contar cuántos
bytes '\n' hay en la entrada (que es, ni más ni menos, cuántas líneas terminadas tiene).
Consigna · Leer de stdin byte a byte y contar solo los bytes que sean '\n'. Al terminar,
mostrar el total.
Entrada · Texto hasta EOF.
Salida · La cantidad de saltos de línea.
Escenario de prueba
printf 'uno\ndos\ntres\n' | ./lineas
Saltos de línea: 3
printf 'sin salto al final' | ./lineas
Saltos de línea: 0
Criterios de aceptación
- Lee con
getcharen uninty corta anteEOF. - Cuenta solo
'\n'(no otros bytes). - Los dos escenarios dan el resultado esperado.
- No hay bucle infinito si la entrada no termina en
'\n'.
PARTE 3 — Leer líneas enteras: fgets
Leer de a un byte está bien, pero muchas veces querés "una línea entera de una". Para eso existe
fgets.
3.1 El cucharón que llena el balde
fgets es como un cucharón: saca de la tubería hasta llenar un balde (un arreglo de char) o
hasta encontrar el fin de la línea, lo que pase primero.
char *fgets(char *s, int n, FILE *stream);
s: el balde (tu arreglo dechar) donde se guarda lo que leyó.n: el tamaño del balde.stream: de qué tubería leer (stdin, en la consola).- Devuelve:
ssi leyó algo, oNULLsi llegó al final o hubo error.
3.2 Tres reglas de oro de fgets
- Lee como máximo
n - 1caracteres. Reserva un lugar para el terminador'\0'. Por eso un balde de 20 guarda 19 letras + el'\0'. - Se detiene en
'\n'… y lo guarda. A diferencia de lo que haría un humano,fgetsdeja el'\n'adentro del balde. Es una fuente clásica de sorpresas (ver Parte 4). - Nunca se desborda si le pasás el tamaño real. Como sabe cuánto mide el balde, para cuando se llena. Es la función segura para leer por consola.
3.3 Leer líneas en un bucle
#include <stdio.h>
int main(void) {
char line[100];
/* fgets devuelve NULL cuando ya no hay más líneas (EOF) o hay error.
Le paso sizeof line, así nunca escribe fuera del arreglo. */
while (fgets(line, sizeof line, stdin) != NULL) {
printf("Leí: %s", line); /* line ya incluye el '\n', por eso no lo agrego */
}
return 0;
}
3.4 Líneas más largas que el balde
Si la línea es más larga que el balde, fgets la corta: llena el balde hasta n-1 y sigue en la
próxima llamada. No se rompe nada, pero tu programa ve la línea partida. Para darte cuenta, el
truco es: si el balde se llenó y NO terminó en '\n', la línea era más larga de lo que entra.
char line[20];
fgets(line, sizeof line, stdin);
int len = strlen(line); /* strlen necesita <string.h> */
if (len > 0 && line[len - 1] != '\n') {
/* el balde se llenó sin encontrar el '\n': la línea venía cortada */
}
3.5 Cuando la línea no entra: descartar el resto
Detectar que una línea se cortó es solo la mitad. La otra mitad es descartar lo que sobró: si no, esa parte se va a leer como si fuera una línea nueva (y vas a procesar datos fantasma).
Después de detectar el corte, "comete" lo que queda de la línea hasta el '\n':
char line[20];
fgets(line, sizeof line, stdin);
int len = strlen(line);
if (len > 0 && line[len - 1] != '\n') {
/* La línea era más larga que el balde: tiro lo que sobra
hasta consumir el '\n' (o llegar a EOF). */
int c;
while ((c = getchar()) != '\n' && c != EOF) {
/* vacío a propósito: solo descarto */
}
}
Ese bucle es un patrón que vas a reusar: en la Parte 4 vuelve a aparecer, con la misma forma, para
limpiar los residuos que deja scanf. Guardátelo.
Práctica de la Parte 3
Ejercicio 14.5 — Protección contra desbordamiento
Conceptos · fgets con búfer fijo, truncamiento, detección de líneas largas, descarte del resto
Contexto · Un lector de comandos tiene que aceptar entradas más largas que su búfer sin desbordar la memoria. Simplificación: búfer estático de 20 bytes y aviso de recorte.
Consigna · Leer un comando con fgets en un búfer estático de 20 bytes. Si la línea era más
larga que el búfer, avisar que se recortó y descartar el resto con el patrón de 3.5, para que no se
lea como una línea nueva.
Entrada · Una línea (posiblemente más larga que el búfer). Salida · El comando leído y, si corresponde, el aviso de recorte.
Escenario de prueba
Entrada: una orden extremadamente larga
Salida:
Comando: "una orden extremada"
Aviso: la línea era más larga que el búfer (20 bytes) y fue recortada.
El búfer de 20 guarda 19 caracteres +
'\0': por eso se veuna orden extremada(19 letras). El resto (mente larga) se descarta.
Criterios de aceptación
fgetsrecibe el tamaño real del búfer (sizeof).- Entradas cortas se leen completas.
- Entradas largas se recortan a 19 caracteres y se avisa.
- El resto de la línea se descarta; no contamina la lectura siguiente.
- No hay desbordamiento ni comportamiento indefinido.
Ejercicio 14.6 — Contador de palabras
Conceptos · fgets, recorrido de una cadena, espacios, bucles
Contexto · Contar palabras es el trabajo de wc -w. Simplificación: contar cuántas "palabras"
(sucesiones de caracteres que no son espacio) hay en la entrada, leyendo línea por línea.
Consigna · Leer la entrada con fgets y contar las palabras. Una palabra empieza cuando pasás de
"espacio" a "no espacio". Contá también la última palabra si la línea no termina en espacio.
Entrada · Texto hasta EOF.
Salida · La cantidad de palabras.
Escenario de prueba
printf 'hola mundo\nesto es una prueba\n' | ./palabras
Palabras: 6
printf ' muchos espacios \n' | ./palabras
Palabras: 2
Criterios de aceptación
- Lee con
fgetsy comprueba que no devolvióNULL. - Cuenta las palabras correctamente, sin importar cuántos espacios haya de más.
- Los dos escenarios dan el resultado esperado.
- No asume que toda línea termina en espacio.
PARTE 4 — El búfer: el villano de la película
Esta es la parte más importante del capítulo. Si entendés el búfer, se te acaban los misterios.
4.1 ¿Por qué getchar parece quedarse dormido?
Escribí este programa y corrélo. Va a leer un byte y mostrarlo:
#include <stdio.h>
int main(void) {
printf("Escribí algo: ");
int c = getchar();
printf("Leí el byte: '%c'\n", c);
return 0;
}
Si lo corrés y escribís hola, no pasa nada hasta que apretás Enter. Recién ahí el programa
continúa. ¿Por qué? Porque hay dos búferes en el medio, y entenderlos es entender todo.
4.2 Los dos búferes (y por qué existen)
Un búfer es una zona de memoria temporal que junta datos antes de pasarlos, para no estar yendo y viniendo al dispositivo en cada byte (que sería lentísimo). Hay dos búferes en serie:
tus teclas → [ búfer de la TERMINAL ] → [ búfer de la LIBRERÍA C ] → tu getchar()
(lo maneja el sistema) (lo maneja tu programa)
se vacía con ENTER entrega byte a byte
- Búfer de la terminal: lo maneja el sistema operativo. Acumula lo que tecleás y solo lo
entrega cuando apretás
Enter. Por esogetchar"espera". - Búfer de la librería C: lo maneja tu programa. Recibe lo que entregó el sistema y lo entrega
a tus
getchar, de a un byte.
4.3 Modo "canónico": el teclado entrega por líneas
Ese comportamiento —"entregá todo junto recién con Enter"— se llama modo canónico y es el
modo por defecto de las terminales. ¿Se puede cambiar para que cada tecla se entregue al instante?
Sí, se llama modo no canónico (o raw), y lo usan los juegos de texto y los editores de pantalla
completa.
Pero ojo: el modo no lo elige
getchar. Lo elige la terminal, y se configura con APIs del sistema (termios en Linux,SetConsoleModeen Windows) que no son estándar de C. Por eso "leer una tecla sin apretarEnter" no es portable. Con las funciones de este capítulo, siempre vas a necesitar elEnter.
4.4 El bug más famoso del principiante: scanf deja el \n pendiente
Ahora sí, el villano. Mirá este clásico:
int age;
char name[50];
scanf("%d", &age); /* el usuario escribe: 30 <Enter> */
fgets(name, sizeof name, stdin); /* ¡lee una línea vacía! */
¿Qué pasó? scanf("%d", ...) lee los dígitos 3 y 0, y para en el primer carácter que no es
dígito: el '\n' del Enter. Ese \n queda en el búfer. Después, fgets ve que "hay una línea
pendiente" (que solo contiene ese \n) y la lee: name queda vacío. El nombre nunca se leyó.
No es un bug de
scanf: es su contrato.%dlee el número, no "la línea". Lo que sobra queda ahí para la próxima lectura. Y la próxima lectura sos vos.
4.5 La solución: "comerse" el resto de la línea
Antes de leer una línea con fgets, hay que descartar lo que quedó pendiente. El patrón es:
/* Descarta lo que quede en la línea actual, hasta consumir el '\n'. */
int c;
while ((c = getchar()) != '\n' && c != EOF) {
/* vacío a propósito: solo tiramos los caracteres */
}
El bucle lee y descarta hasta encontrar el '\n' (lo consume) o hasta EOF. Después de esto, el
búfer queda limpio y "alineado" al comienzo de la línea siguiente.
4.6 Por qué fflush(stdin) NO se usa (aunque te lo recomienden)
Mucha gente "limpia" el búfer con fflush(stdin). No lo hagas. fflush está definido por el
estándar solo para flujos de salida (para vaciar lo que escribiste). Aplicado a la entrada, el
comportamiento es indefinido:
fflush(stdin); /* ¡NO PORTABLE! El estándar no define qué hace */
Algunos compiladores (MSVC, en Windows) le dan un sentido útil, y por eso el código con fflush(stdin)
"funciona" en esa máquina. Pero no es estándar y puede fallar en otra. La forma portable de
limpiar es el bucle de 4.5.
Práctica de la Parte 4
Ejercicio 14.7 — Corrección de búfer tras scanf
Conceptos · scanf, fgets, \n pendiente, limpieza del búfer
Contexto · El error más común al mezclar scanf y fgets: el \n que scanf deja pendiente
"come" la línea siguiente. Simplificación: leer un número y después un nombre, sin perderlo.
Consigna · Pedir un número entero con scanf y luego una cadena corta con fgets. Implantá la
limpieza del búfer intermedia (la de 4.5) para que el \n pendiente no arruine el nombre.
Entrada · Dos líneas: un número y un texto. Salida · El número y el nombre, cada uno en su línea.
Escenario de prueba
Entrada:
42
Ana
Salida:
Número: 42
Nombre: Ana
Para comprobar que lo hiciste bien: sin la limpieza, la salida sería
Nombre:(vacío), porquefgetsleería el'\n'pendiente en vez deAna.
Criterios de aceptación
- Comprueba el retorno de
scanf. - Limpia el búfer pendiente de forma portable (bucle de lectura), no con
fflush. fgetsobtiene el nombre, no una línea vacía.- Muestra número y nombre correctos.
Ejercicio 14.8 — Cuestionario de datos mixtos
Conceptos · scanf, getchar, fgets, saltear blancos, orden de lectura, residuos
Contexto · Un alta de usuario mezcla un ID numérico, una marca sí/no y un nombre completo (con espacios). Simplificación: capturar los tres desde la misma entrada, sin residuos entre lecturas.
Consigna · Leer, en este orden y desde la misma entrada:
- el ID numérico con
scanf("%d", ...); - saltar los blancos que quedaron después del número (espacios y tabs), leyendo con
getchar(); - la marca
s/n(un solo carácter) congetchar(); - descartar el resto de la línea con el patrón de 4.5, para no arrastrar residuos;
- el nombre completo con
fgets()(puede tener espacios).
Garantizá que no queden caracteres residuales entre una lectura y la siguiente.
Entrada · Primera línea: ID marca (separados por un espacio); segunda línea: el nombre.
Salida · Los tres datos, etiquetados.
Escenario de prueba
Entrada:
1001 s
Juan Gómez
Salida:
ID: 1001
Activo: s
Nombre: Juan Gómez
Criterios de aceptación
- Lee los tres datos en el orden pedido, desde la misma entrada.
- El nombre conserva los espacios internos (
Juan Gómez). - No quedan residuos: la lectura del nombre no está "corrida".
- Comprueba los valores de retorno.
Ejercicio 14.9 — Menú de opciones de longitud fija
Conceptos · getchar, consumir el resto de la línea, bucles, validación
Contexto · Los menús de consola leen una opción. Si el usuario escribe una cadena larga, hay que quedarse con el primer carácter y no arrastrar el resto a la próxima vuelta.
Consigna · Mostrar un menú ([A] alta [B] baja [C] consulta [Q] salir), leer la opción con
getchar() y, si el usuario escribió más de un carácter, consumir el resto de la línea antes
de la iteración siguiente. Repetir hasta Q o hasta EOF.
Entrada · Líneas de opciones. Salida · El eco de cada opción elegida y, al final, el cierre.
Escenario de prueba
Entrada:
xyz
C
(Ctrl+Z / Ctrl+D para terminar)
Salida:
[A] alta [B] baja [C] consulta [Q] salir
> Elegiste: x
[A] alta [B] baja [C] consulta [Q] salir
> Elegiste: C
[A] alta [B] baja [C] consulta [Q] salir
> Fin del menú
Fijate que la entrada
xyzeligexy descartayz: por eso la vuelta siguiente arranca limpia.
Criterios de aceptación
- Lee un único carácter por iteración.
- Si se escribió más de un carácter, consume el resto de la línea.
- La opción siguiente no está contaminada por residuos.
- Termina con
Qy también anteEOF, sin bucle infinito.
PARTE 5 — Leer con formato: scanf y sus mañas
fgets te da la línea. scanf te da el dato que le pedís, en el formato que le indiques. Es
potente y cómodo… y traicionero si no respetás sus reglas.
5.1 La idea: un mini-lenguaje de formatos
scanf recibe una cadena de formato donde le describís qué querés leer. Cada conversión
empieza con %:
int n;
double price;
char letter;
scanf("%d", &n); /* lee un entero decimal */
scanf("%lf", &price); /* lee un número con coma (double) */
scanf(" %c", &letter); /* lee UN carácter (el espacio salta blancos) */
Siempre se le pasa la dirección de la variable (con &): &n, no n. Suena obvio, pero es
el error N.º 1.
5.2 Qué devuelve scanf: el dato clave para no romperte
scanf no devuelve lo que leyó. Devuelve cuántas variables llenó:
scanf("%d", &n)→ devuelve1si leyó un número bien.scanf("%d %d", &a, &b)→ devuelve2si leyó los dos.0si el primer dato no coincidía con el formato (por ejemplo, pedís%dy hayabc).EOFsi la entrada terminó antes de leer nada.
Por eso, siempre se compara el retorno con lo esperado. Es la única forma de saber si salió bien.
5.3 %c, %s y %[...]: tres formas muy distintas de leer texto
| Formato | Qué lee | ¿Salta espacios? | Nota |
|---|---|---|---|
%c |
un carácter | No | Si el próximo byte es espacio o \n, ese te da |
%s |
una palabra (sin espacios) | Sí | Peligroso: sin ancho, puede desbordar |
%[...] |
los caracteres de un conjunto | No | %[^\n] = "todo hasta el \n" |
Ejemplos de cómo se comportan ante la entrada hola mundo:
scanf("%c", &c)→ te da el primer espacio, no lah.scanf("%s", word)→ salta el espacio y te dahola(se detiene en el espacio).scanf("%[^\n]", line)→ te dahola mundocompleto (lee todo menos el\n).
El espacio en el formato. Un espacio en la cadena de formato significa "salteen cero o más espacios". Por eso
" %c"(con espacio antes de%c) salta el\npendiente y recién ahí lee el carácter. Es el truco para leer un%cdespués de un%d.
5.4 Los dos superpoderes (y una advertencia): ancho y *
Ancho máximo. Un número entre % y la letra limita cuánto lee: %19s lee como máximo 19
caracteres (más el '\0'), para un búfer de 20. Nunca uses %s sin ancho: es el peligro de
desbordar.
char word[20];
scanf("%19s", word); /* a lo sumo 19 letras + '\0': seguro */
Supresión (*). Un * después del % significa "leé esto, pero no lo guardes". Sirve para
saltear prefijos:
int value;
scanf("%*s %d", &value); /* lee y TIRA una palabra; guarda solo el número */
/* ante "ID: 12345", descarta "ID:" y guarda 12345 */
La advertencia (%n). %n no lee: guarda cuántos caracteres se leyeron hasta ahí. Sirve para
saber "por dónde quedó", pero se lo considera riesgoso: no lo uses con formatos que vengan de afuera.
5.5 El peligro del tipo equivocado
scanf no sabe qué tipo le pasaste: se guía solo por el formato. Si el formato dice %d
(espera un int *) y le pasás un double *, escribe un int donde vos esperabas un double:
comportamiento indefinido. Puede dar basura, corromper memoria, o "andar" por casualidad hasta que
deje de andar.
Regla de oro de
scanf: el formato y el puntero tienen que coincidir exactamente. El compilador no puede revisarlo (por los...), así que la responsabilidad es toda tuya. Ojo con el clásico: enscanf, para undoublese usa%lf(¡no%f, como enprintf!).
5.6 Falta %i, %u, %x… y gets ya no existe
%iadivina la base (decimal,0xhex,0octal);%dsolo decimal.%ues para enteros sin signo;%fparafloat;%lfparadouble.getsfue eliminada del estándar (C11) porque leía una línea sin límite de tamaño: no podía saber cuándo parar y desbordaba el búfer. Su reemplazo seguro esfgetscon tamaño.
5.7 sscanf: el mismo scanf, pero sobre una cadena
sscanf es idéntico a scanf, salvo que no lee de un flujo (stdin), sino de una cadena que
ya tenés en memoria. Es el compañero natural de fgets: leés la línea una vez y después la
interpretás sin volver a tocar stdin.
#include <stdio.h>
int main(void) {
char line[100];
if (fgets(line, sizeof line, stdin) == NULL) {
return 0;
}
int a, b;
/* sscanf(cadena, formato, direcciones...):
igual que scanf, pero la "fuente" es `line` en vez de stdin.
Devuelve cuántos valores asignó (2 si leyó los dos enteros). */
if (sscanf(line, "%d %d", &a, &b) == 2) {
printf("suma: %d\n", a + b);
}
return 0;
}
Por qué conviene. Leer una vez con
fgetsy después analizar consscanfes más seguro que encadenarscanfsobrestdin: controlás el tamaño de la línea, la ves completa y, si el formato no cierra, no dejaste residuos en el flujo.
Práctica de la Parte 5
Ejercicio 14.10 — Validador de edad numérica
Conceptos · scanf, valor de retorno, fallo de conversión, descarte de entrada inválida
Contexto · Los formularios tienen que sobrevivir a que alguien escriba texto donde se pide un número. Simplificación: pedir una edad y detectar el error sin comportarse de forma errática.
Consigna · Pedir la edad con scanf("%d", &age). Si la conversión falla (el retorno no es
1), mostrar un mensaje de entrada inválida y no usar age. El programa no debe bloquearse ni
leer datos basura.
Entrada · Una línea con un número (caso válido) o con texto (caso inválido). Salida · La edad registrada, o el mensaje de error.
Escenario de prueba
Entrada: 34
Salida: Edad registrada: 34
Entrada: treinta
Salida: Entrada inválida: se esperaba un número entero.
Criterios de aceptación
- Comprueba
scanf(...) == 1. - Con texto, informa el error y no usa un valor no inicializado.
- Con un número válido, lo muestra correctamente.
- No deja la entrada "sucia" para una lectura posterior.
Ejercicio 14.11 — Extracción de tokens con formato avanzado
Conceptos · scanf con ancho máximo (%19s), supresión (*), valor de retorno
Contexto · Muchas líneas traen etiquetas (ID: 12345) y solo interesa el valor. Simplificación:
extraer el número descartando el prefijo, con un solo scanf.
Consigna · Leer una línea con etiqueta y extraer solo el valor numérico, descartando el
prefijo textual. Usá un ancho máximo para el token del prefijo y el modificador * para que
scanf no lo guarde.
Entrada · Una línea con formato ETIQUETA: número.
Salida · El valor numérico con una etiqueta clara.
Escenario de prueba
Entrada: ID: 12345
Salida: valor = 12345
Entrada: REF: 00042
Salida: valor = 42
Criterios de aceptación
- Usa el ancho máximo en el token descartado (
%19so similar). - Usa
*para no asignar la etiqueta. - Extrae el número correctamente en ambos escenarios.
- Comprueba el retorno de
scanf(== 1).
Ejercicio 14.12 — La calculadora de una línea
Conceptos · scanf con formato mixto (%d %c %d), valor de retorno
Contexto · Una calculadora de bolsillo acepta número operador número. Simplificación: leer una
cuenta de suma o resta con un solo scanf.
Consigna · Leer una línea con el formato número operador número (por ejemplo 8 + 5) usando
scanf("%d %c %d", &a, &op, &b). Comprobar que se asignaron 3 valores. Si el operador es +,
mostrar la suma; si es -, la resta; si es otro, avisar que no se reconoce.
Entrada · Una cuenta de una línea. Salida · El resultado, o el aviso de operador no reconocido.
Escenario de prueba
Entrada: 8 + 5
Salida: Resultado: 13
Entrada: 10 - 4
Salida: Resultado: 6
Entrada: 3 x 4
Salida: Operador no reconocido: 'x'
Criterios de aceptación
- Usa
scanf("%d %c %d", ...)y comprueba que el retorno sea3. - Resuelve suma y resta correctamente.
- Avisa en el caso de operador no reconocido.
- No se rompe si la línea no trae los tres datos.
PARTE 6 — Caracteres: cuando "una letra" pesa más de un byte
Hasta acá contamos bytes. Pero, ¿un byte es siempre "una letra"? Spoiler: no.
6.1 Byte no es lo mismo que carácter
- Un byte es una unidad de almacenamiento:
sizeof(char)es siempre1, y en la práctica son 8 bits. Es una medida de cuánto ocupa. - Un carácter es una noción humana: "la letra
á", "el símbolo€". Es una medida de significado.
La relación entre ambos la define la codificación: cuántos bytes (y con qué valores) representan cada carácter.
6.2 EOF, '\0', '\n' y '\r': cuatro parecidos que no se parecen
Antes de seguir, despejemos cuatro nombres que se confunden siempre:
| Símbolo | Qué es | Valor | ¿Es un byte de dato? |
|---|---|---|---|
EOF |
"se acabó la entrada" | -1 |
No (es una señal) |
'\0' |
carácter nulo, terminador de cadenas | 0 |
Sí |
'\n' |
fin de línea (LF) | 10 (0x0A) |
Sí |
'\r' |
retorno de carro (CR) | 13 (0x0D) |
Sí |
6.3 ASCII, UTF-8 y la á de dos bytes
ASCII es la codificación clásica: 128 caracteres (letras inglesas, dígitos, signos), todos de
1 byte. 'A' es 65, '\n' es 10.
UTF-8 es la codificación de hoy (Linux, macOS, la web) y es de longitud variable:
- Los caracteres ASCII ocupan 1 byte (compatibilidad total).
- Los demás ocupan 2, 3 o 4 bytes. El primer byte "avisa" cuántos siguen.
| Carácter | Bytes (hex) | ¿Cuántos? |
|---|---|---|
A |
41 |
1 byte |
ñ |
C3 B1 |
2 bytes |
á |
C3 A1 |
2 bytes |
€ |
E2 82 AC |
3 bytes |
😀 |
F0 9F 98 80 |
4 bytes |
6.4 Por qué esto te importa de verdad
Como getchar entrega bytes (no glifos), leer un texto con acentos tiene consecuencias concretas:
- Si tecleás
á,getcharte va a dar dos bytes (0xC3y luego0xA1). Dos llamadas, un símbolo. strlen("á")da 2, no1.- Si recorrés "carácter por carácter" con
getchar, vas a partir los multibyte al medio.
Reglas prácticas:
- Separá las capas. ¿Querés contar bytes o símbolos? No es lo mismo.
- Cuando examinés bytes crudos, usá
unsigned char. Comparar0xC3contra uncharcon signo trae resultados confusos. - Un
charno alcanza para todo símbolo. Para eso existenwchar_tychar32_t(capítulo16-tipos-stddef). - No adivines la codificación. Si los acentos salen "raros", casi siempre es que la codificación del texto no coincide con la que espera la terminal.
Práctica de la Parte 6
Ejercicio 14.13 — Filtro de terminadores de línea
Conceptos · getchar, EOF, '\n', '\r', '\0', distinción de símbolos
Contexto · Los analizadores de texto necesitan distinguir los símbolos de control de los caracteres comunes. Simplificación: etiquetar cada símbolo especial.
Consigna · Leer de stdin byte a byte y, por cada símbolo:
'\n'→ imprimir<LF>;'\r'→ imprimir<CR>;'\0'→ imprimir<NUL>;- cualquier otro → imprimir el carácter tal cual;
- al llegar a
EOF→ imprimir<EOF>y terminar.
Entrada · Secuencia de bytes hasta EOF.
Salida · El carácter (o su etiqueta) por cada símbolo; <EOF> al final.
Escenario de prueba
printf 'a\nb\rc\0d' | ./filtro
a
<LF>
b
<CR>
c
<NUL>
d
<EOF>
'\0'solo aparece si la entrada es un flujo con bytes0x00(por ejemplo, una tubería); no se puede teclear en una terminal.
Criterios de aceptación
- Distingue
'\n','\r','\0'yEOFcorrectamente. - Imprime el resto de los caracteres sin alterarlos.
- Detecta
'\0'como byte real0x00(no lo confunde conEOF). - Termina en el primer
EOF, con la etiqueta<EOF>incluida.
Ejercicio 14.14 — Radiografía de un acento
Conceptos · getchar, bytes altos, UTF-8 multibyte, unsigned char
Contexto · Ver "por dentro" cómo se guarda una letra acentuada es la mejor forma de entender multibyte. Simplificación: mostrar cada byte de la entrada en hexadecimal.
Consigna · Leer de stdin byte a byte y mostrar cada uno en hexadecimal (por ejemplo C3 A1),
separados por espacio, hasta EOF. Al final, mostrar también el total de bytes.
Entrada · Cualquier texto hasta EOF (probá con á, ñ, €).
Salida · La seguidilla de bytes en hex y el total.
Escenario de prueba
printf 'á' | ./radio
Bytes: C3 A1
Total: 2 bytes
printf 'A' | ./radio
Bytes: 41
Total: 1 byte
Lo que tenés que ver:
áson dos bytes (C3 A1),Aes uno (41). Ahí está el multibyte, en la mano.
Criterios de aceptación
- Lee con
getcharen uninty corta anteEOF. - Muestra cada byte en hexadecimal, con dos dígitos (por ejemplo
0A, noA). - Informa el total de bytes leídos.
- El acento da 2 bytes y la
Ada 1 byte.
PARTE 7 — Robustez: que no se rompa nunca
Un programa frágil asume que la entrada es perfecta. Un programa robusto asume lo contrario. Esta parte es corta pero salvavidas.
7.1 Comprobá siempre los retornos
Cada lectura te avisa cómo salió. Ignorar el aviso es pedir un desastre. Este es el reflejo que tenés que automatizar:
int c;
while ((c = getchar()) != EOF) { /* ... */ } /* getchar contra EOF */
char line[100];
if (fgets(line, sizeof line, stdin) == NULL) { /* fgets contra NULL */
/* fin de entrada o error */
}
int n;
if (scanf("%d", &n) != 1) { /* scanf contra lo esperado */
/* no se leyó un número */
}
7.2 feof y ferror: ¿se acabó o se rompió?
Cuando una lectura falla, puede ser por dos motivos distintos: se acabó la entrada (normal) o hubo un error (falla real). Dos funciones los separan:
int feof(FILE *stream); /* ¿se llegó al final de la entrada? */
int ferror(FILE *stream); /* ¿hubo un error de lectura/escritura? */
Y perror imprime un mensaje legible del último error, en stderr:
if (ferror(stdin)) {
perror("stdin"); /* imprime algo como: stdin: <motivo> */
}
clearerr(stream) borra los indicadores de error y de fin de archivo. Se usa si querés seguir
leyendo después de haber tocado el final (si no, el flujo "queda pegado" en EOF).
7.3 El bug estrella: el bucle infinito
Si la entrada termina y tu bucle no corta, vuelve a leer, recibe EOF de nuevo, y gira para
siempre. El patrón correcto es siempre tener una condición de corte:
int c;
while ((c = getchar()) != EOF) { /* procesar c */ }
/* Con scanf: cortar si no asignó lo esperado. */
int n;
while (scanf("%d", &n) == 1) { /* procesar n */ }
Caso especial. Si
scanffalla porque el formato no coincide, deja el carácter culpable sin consumir. Si no lo descartás, el bucle lo vuelve a encontrar y no avanza: bucle infinito. La solución es descartar el carácter problemático (o el resto de la línea) o abandonar la lectura.
7.4 La entrada puede ser maliciosa
Si la entrada viene de cualquiera, no confíes:
- Acotá siempre los búferes (nunca
%ssin ancho, nuncagets). - Comprobá rangos: que un entero entre en el tipo, que un índice sea válido.
- Convertí con control de error, en vez de confiar en conversiones "optimistas".
7.5 strtol y strtod: convertir texto a número sin sorpresas
atoi y atof no avisan cuando la conversión falla: devuelven 0 y listo. Para validar en serio,
strtol (texto → long) y strtod (texto → double) te dan tres señales en una sola llamada:
#include <stdlib.h> /* strtol, strtod */
#include <errno.h> /* errno */
/* strtol(cadena, endptr, base):
- Devuelve el valor convertido.
- endptr (puntero al puntero): queda apuntando al primer carácter NO convertido.
· si endptr == inicio → no se convirtió nada (no era un número);
· si *endptr == '\0' → se consumió toda la cadena (todo era el número);
· si *endptr apunta a otra cosa → sobra basura después del número.
- errno se pone en ERANGE si el valor no entra en un long.
Por eso se limpia errno = 0 ANTES de llamar. */
char *endptr;
errno = 0;
long value = strtol(text, &endptr, 10);
Con esas tres señales (¿convirtió algo?, ¿sobró basura?, ¿hubo desborde?) tenés todo para decidir si la
entrada es válida. strtod funciona igual, pero para double.
Práctica de la Parte 7
Ejercicio 14.15 — Bucle robusto ante cierre de terminal
Conceptos · getchar, EOF, corte de bucles, fin de entrada por teclado
Contexto · Un programa interactivo tiene que terminar limpio cuando el usuario cierra la
entrada, sin quedar girando. Simplificación: contar caracteres y líneas hasta EOF.
Consigna · Leer de stdin con getchar() hasta EOF. Contar los caracteres y las líneas
(cantidad de '\n'). Al detectar EOF, mostrar el resumen y un mensaje de cierre. No debe haber
bucle infinito.
Entrada · Líneas de texto, cerradas con Ctrl+Z (Windows) o Ctrl+D (Linux/macOS). Salida · Conteo de caracteres y líneas, y mensaje de fin.
Escenario de prueba
Entrada:
hola
mundo
(Ctrl+Z + Enter / Ctrl+D)
Salida:
Leídos 11 caracteres en 2 líneas.
Fin de la entrada (EOF). Chau.
hola\nson 5 caracteres ymundo\nson 6: 11 en total y 2 saltos de línea.
Criterios de aceptación
- El bucle corta ante
EOF(no hay giro infinito). - Cuenta caracteres y líneas correctamente.
- Con la entrada del ejemplo, informa 11 caracteres y 2 líneas.
- Muestra un mensaje de cierre y termina.
Ejercicio 14.16 — Validador de número a prueba de balas
Conceptos · fgets, strtol, validación de rango, detección de basura
Contexto · Un dato confiable no es solo "un número": es un número dentro de un rango y sin basura alrededor. Simplificación: leer un puerto de red (0 a 65535) y validarlo del todo.
Consigna · Leer una línea con fgets y convertirla con strtol. La entrada es válida solo si:
se convirtió algo, no sobra nada (salvo el '\n'), y el valor está entre 0 y 65535. Si algo
falla, informar el motivo exacto.
Entrada · Una línea de texto. Salida · El puerto validado, o el motivo del rechazo.
Escenario de prueba
Entrada: 8080
Salida: Puerto válido: 8080
Entrada: 8080x
Salida: Error: hay caracteres extra después del número ('x')
Entrada: 70000
Salida: Error: 70000 está fuera del rango 0..65535
Entrada: puerto
Salida: Error: 'puerto' no es un número
Criterios de aceptación
- Lee la línea con
fgetsy comprueba el retorno. - Usa
strtolcon el puntero de fin (endptr) para saber hasta dónde convirtió. - Detecta: basura antes, basura después, vacío y fuera de rango.
- Un valor válido se acepta; los cuatro escenarios de error coinciden.
PARTE 8 — Proyecto final: tu propia consola de comandos
Llegó la hora de juntar todo. Vas a construir un intérprete de comandos mínimo: una versión de juguete de tu terminal.
8.1 Qué vas a construir
Un programa que muestre un prompt > , espere una orden y reaccione. No va a ejecutar programas
reales: solo va a entender unos pocos comandos. Pero todo lo que aprendiste va a estar adentro:
leer líneas acotadas, interpretar el comando, validar números, manejar residuos del búfer y cerrar con
EOF o con salir.
8.2 Los comandos a entender
| Orden | Qué hace |
|---|---|
eco <texto> |
Imprime <texto> tal cual (puede tener espacios). |
suma <a> <b> |
Imprime la suma de dos enteros. |
salir |
Termina el programa. |
| (otra cosa) | Error de comando desconocido. |
Ejercicio final
Ejercicio 14.17 — (JEFE) Simulador de consola de comandos interactiva
Conceptos · fgets, sscanf, stdin/stdout/stderr, limpieza de búfer, validación numérica,
EOF, corte de bucles, comandos con formato.
Contexto · Un intérprete de comandos (un "shell" mínimo) es el ejercicio que reúne todo el capítulo: lee órdenes, interpreta argumentos, valida errores sin bloquearse y termina limpio. Es una simplificación de una consola real: no ejecuta programas, solo reconoce unos pocos comandos.
Consigna · Desarrollar una aplicación de línea de comandos que:
- Muestre un prompt
>y lea órdenes del usuario combinandofgetsysscanfsegún el comando. - Reconozca los comandos de la tabla de 8.2 (
eco,suma,salir). - Maneje correctamente los residuos de búfer cuando la entrada excede el tamaño esperado (comando demasiado largo, o una línea con más datos de los que consume el formato).
- Valide errores de formato numérico (por ejemplo,
suma 4 abc) sin bloquearse. - Termine de forma limpia al detectar
EOFo al recibirsalir, sin bucles infinitos ni comportamiento indefinido.
Entrada · Órdenes por stdin, una por línea; la sesión cierra con salir o EOF.
Salida · El resultado de cada comando por stdout; los errores por stderr; un cierre al final.
Escenario de prueba
Entrada:
eco hola mundo
suma 4 7
suma 4 abc
xyz
salir
stdout:
> hola mundo
> 11
> Error: 'abc' no es un número válido
> Error: comando desconocido: xyz
> Chau.
Entrada:
(Ctrl+Z + Enter / Ctrl+D)
stdout:
> Fin de entrada (EOF). Chau.
Caso de orden demasiado larga (con búfer de 128 bytes):
Entrada: eco <200 caracteres>
Salida: Error: la orden supera el tamaño máximo del búfer.
Identificadores sugeridos · line, command, arg1, arg2, LINE_SIZE, matched, running
Restricciones/pistas
- Leer cada orden con
fgetsen un búfer acotado y detectar truncamiento (3.4). - Para
suma, interpretar los dos enteros consscanfsobre la línea ya leída (más seguro quescanfdirecto sobrestdin) y comprobar que devolvió2; si no, error de formato. - Descartar el resto de la línea cuando haga falta (4.5). No usar
fflush(stdin). - Cortar ante
EOF:fgetsdevuelveNULL. - Separar la salida normal (
stdout) de los errores (stderr).
Criterios de aceptación
- Muestra el prompt y lee órdenes con
fgetsacotado. ecoimprime el texto conservando los espacios internos.sumacon dos enteros válidos imprime la suma correcta.sumacon un argumento no numérico informa el error y continúa (no se bloquea).- Un comando desconocido informa el error y continúa.
- Una orden que excede el búfer se detecta y no corrompe la memoria ni la lectura siguiente.
saliry elEOFterminan el programa limpiamente (con mensaje de cierre).- No hay bucles infinitos ni comportamiento indefinido ante entradas inesperadas.
- Errores por
stderr, resultados porstdout.
Cierre: lo que te llevás
Si tuvieras que quedarte con una sola idea, que sea esta: leer de la consola es leer de una tubería con búfer. Todo lo demás se deduce de ahí.
- Tus programas tienen tres tuberías (
stdin,stdout,stderr), y el shell decide a qué está conectada cada una. Tu código no cambia. - Elegí la función por su trabajo:
getchar(un byte),fgets(una línea, seguro),scanf(con formato, cómodo pero resbaladizo). EOFes una señal (unintnegativo), no un carácter. Nunca lo guardes en unchar.- El búfer causa los misterios:
scanfdeja el\npendiente; por eso existe la limpieza de línea, y por esofflush(stdin)está prohibido. - La robustez es una decisión: comprobar retornos, acotar búferes, validar datos y cortar ante
EOF. - Un carácter no siempre es un byte: una
áson dos bytes.
Y el puente hacia lo que viene: para C, un archivo es un flujo más. En 15-archivos vas a reusar
exactamente estas ideas —fgetc, fgets, fscanf, feof, los búferes, EOF— solo que sobre un
flujo abierto con fopen en lugar de sobre stdin. Si dominaste este capítulo, el de archivos es
casi el mismo capítulo con una apertura y un cierre.