300052b921
This chapter covers the most commonly used types in C, including size_t, wchar_t, NULL, bool, and fixed-width integers. It explains the importance of using these types over int for portability and correctness, along with practical examples and exercises to reinforce understanding.
1557 lines
59 KiB
Markdown
1557 lines
59 KiB
Markdown
# Capítulo 13 — 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`), `if` y bucles (`04`), funciones (`05`) y arreglos y cadenas (`06`).
|
||
>
|
||
> Es el **puente** hacia el capítulo de archivos (`14-archivos`). Todo lo que aprendas acá sobre
|
||
> `stdin`, búferes y `EOF` lo 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: `13.1`, `13.2`, `13.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**:
|
||
|
||
```text
|
||
(de dónde vengan los datos)
|
||
│
|
||
▼
|
||
┌───────────┐
|
||
│ stdin │ ← ENTRADA ESTÁNDAR (por acá entran los datos)
|
||
└─────┬─────┘
|
||
│
|
||
▼
|
||
╔══════════════╗
|
||
║ TU PROGRAMA ║
|
||
╚══╤═══════╤═══╝
|
||
│ │
|
||
┌──────▼──┐ ┌─▼────────┐
|
||
│ stdout │ │ stderr │ ← DOS salidas
|
||
└────┬────┘ └────┬─────┘
|
||
│ │
|
||
▼ ▼
|
||
(salida normal) (errores)
|
||
```
|
||
|
||
- **`stdin`** es la tubería de **entrada**. Por ahí entran los datos.
|
||
- **`stdout`** es la tubería de **salida normal**. Por ahí sale el resultado del programa.
|
||
- **`stderr`** es 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**.
|
||
|
||
```c
|
||
#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 misma `stdin` puede 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.
|
||
|
||
```bash
|
||
$ 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.
|
||
|
||
```bash
|
||
$ 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.
|
||
|
||
```bash
|
||
$ cat < datos.txt
|
||
primera línea del archivo
|
||
segunda línea del archivo
|
||
```
|
||
|
||
> **Momento "ajá".** En los tres casos, `cat` hizo lo mismo internamente: leer de `stdin` y escribir en
|
||
> `stdout`. **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:
|
||
|
||
```bash
|
||
$ 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:
|
||
|
||
```bash
|
||
$ 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`, y `cmd > 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`:
|
||
|
||
```c
|
||
#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**:
|
||
|
||
```bash
|
||
$ ./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 13.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**
|
||
|
||
```text
|
||
$ ./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 `>` y `2>`, cada mensaje cae en su archivo y ninguno aparece en el otro.
|
||
- [ ] Usa `fprintf` indicando el flujo (`stdout`/`stderr`) en cada escritura.
|
||
|
||
### Ejercicio 13.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 13.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**:
|
||
|
||
1. sin redirección;
|
||
2. `./programa > out.txt`;
|
||
3. `./programa 2> err.txt`;
|
||
4. `./programa > out.txt 2> err.txt`;
|
||
5. `./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**
|
||
|
||
```text
|
||
$ ./dos > out.txt 2> err.txt
|
||
$ cat out.txt
|
||
salida normal
|
||
|
||
$ cat err.txt
|
||
mensaje de error
|
||
```
|
||
|
||
```text
|
||
$ ./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**.
|
||
|
||
```c
|
||
#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:
|
||
|
||
```text
|
||
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'`).
|
||
|
||
```text
|
||
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.** `getchar` te devuelve el **byte del carácter**, no el valor del número. Cuando lee
|
||
> `'3'`, te da un byte (cuyo valor es `51`), no el número `3`. Armar el número `304` a partir de esos
|
||
> dígitos es un trabajo **tuyo**, no de `getchar`. Por eso, para leer números "de verdad", más adelante
|
||
> vas a usar `scanf` (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.
|
||
|
||
```c
|
||
#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`.
|
||
|
||
```text
|
||
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 `-1` es `EOF`! Entonces un **dato real** se confunde con "se acabó".
|
||
- O al revés: comparás contra `EOF` y 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`**.
|
||
|
||
```c
|
||
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:
|
||
|
||
```c
|
||
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 a `stdin`: comodísima para la consola.
|
||
- **`getc(stdin)`** y **`fgetc(stdin)`** hacen lo mismo que `getchar()`, pero sirven para **cualquier**
|
||
flujo (muy útil cuando llegues a los archivos).
|
||
- La única diferencia fina: `getc` puede estar implementada como **macro** y `fgetc` **siempre** es
|
||
función. En la práctica, usá `getchar` para la consola y `fgetc(fp)` para un archivo.
|
||
|
||
## Práctica de la Parte 2
|
||
|
||
### Ejercicio 13.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**
|
||
|
||
```text
|
||
printf 'hola' | ./contador
|
||
Caracteres leídos: 4
|
||
```
|
||
|
||
```text
|
||
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 un `char`, el programa corta en el primer `0xFF` y te da **0**.
|
||
|
||
**Criterios de aceptación**
|
||
- [ ] Usa `int` para la lectura y para el contador.
|
||
- [ ] Cuenta **todos** los bytes, incluidos `0x00` y `0xFF`.
|
||
- [ ] Termina limpio en `EOF`, sin bucle infinito.
|
||
- [ ] El resultado coincide con el tamaño real de la entrada.
|
||
|
||
### Ejercicio 13.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**
|
||
|
||
```text
|
||
printf 'uno\ndos\ntres\n' | ./lineas
|
||
Saltos de línea: 3
|
||
```
|
||
|
||
```text
|
||
printf 'sin salto al final' | ./lineas
|
||
Saltos de línea: 0
|
||
```
|
||
|
||
**Criterios de aceptación**
|
||
- [ ] Lee con `getchar` en un `int` y corta ante `EOF`.
|
||
- [ ] 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.
|
||
|
||
```c
|
||
char *fgets(char *s, int n, FILE *stream);
|
||
```
|
||
|
||
- **`s`**: el **balde** (tu arreglo de `char`) donde se guarda lo que leyó.
|
||
- **`n`**: el **tamaño** del balde.
|
||
- **`stream`**: de qué tubería leer (`stdin`, en la consola).
|
||
- **Devuelve**: `s` si leyó algo, o **`NULL`** si llegó al final o hubo error.
|
||
|
||
## 3.2 Tres reglas de oro de `fgets`
|
||
|
||
1. **Lee como máximo `n - 1` caracteres.** Reserva un lugar para el terminador `'\0'`. Por eso un
|
||
balde de 20 guarda **19** letras + el `'\0'`.
|
||
2. **Se detiene en `'\n'`… y lo guarda.** A diferencia de lo que haría un humano, `fgets` **deja el
|
||
`'\n'` adentro** del balde. Es una fuente clásica de sorpresas (ver Parte 4).
|
||
3. **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
|
||
|
||
```c
|
||
#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.
|
||
|
||
```c
|
||
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'`:
|
||
|
||
```c
|
||
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 13.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**
|
||
|
||
```text
|
||
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 ve `una orden extremada` (19 letras).
|
||
> El resto (`mente larga`) se descarta.
|
||
|
||
**Criterios de aceptación**
|
||
- [ ] `fgets` recibe 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 13.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**
|
||
|
||
```text
|
||
printf 'hola mundo\nesto es una prueba\n' | ./palabras
|
||
Palabras: 6
|
||
```
|
||
|
||
```text
|
||
printf ' muchos espacios \n' | ./palabras
|
||
Palabras: 2
|
||
```
|
||
|
||
**Criterios de aceptación**
|
||
- [ ] Lee con `fgets` y 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:
|
||
|
||
```c
|
||
#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:
|
||
|
||
```text
|
||
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 eso `getchar` "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, `SetConsoleMode` en Windows) que **no son estándar de C**. Por eso
|
||
> "leer una tecla sin apretar `Enter`" **no es portable**. Con las funciones de este capítulo, siempre
|
||
> vas a necesitar el `Enter`.
|
||
|
||
## 4.4 El bug más famoso del principiante: `scanf` deja el `\n` pendiente
|
||
|
||
Ahora sí, el villano. Mirá este clásico:
|
||
|
||
```c
|
||
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**. `%d` lee **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:
|
||
|
||
```c
|
||
/* 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**:
|
||
|
||
```c
|
||
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 13.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**
|
||
|
||
```text
|
||
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), porque
|
||
> `fgets` leería el `'\n'` pendiente en vez de `Ana`.
|
||
|
||
**Criterios de aceptación**
|
||
- [ ] Comprueba el retorno de `scanf`.
|
||
- [ ] Limpia el búfer pendiente de forma **portable** (bucle de lectura), **no** con `fflush`.
|
||
- [ ] `fgets` obtiene el nombre, **no** una línea vacía.
|
||
- [ ] Muestra número y nombre correctos.
|
||
|
||
### Ejercicio 13.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:
|
||
|
||
1. el **ID** numérico con `scanf("%d", ...)`;
|
||
2. **saltar los blancos** que quedaron después del número (espacios y tabs), leyendo con `getchar()`;
|
||
3. la **marca** `s`/`n` (un solo carácter) con `getchar()`;
|
||
4. **descartar el resto de la línea** con el patrón de 4.5, para no arrastrar residuos;
|
||
5. 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**
|
||
|
||
```text
|
||
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 13.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**
|
||
|
||
```text
|
||
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 `xyz` elige `x` y **descarta** `yz`: 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 `Q` y también ante `EOF`, 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 `%`:
|
||
|
||
```c
|
||
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)` → devuelve `1` si leyó un número bien.
|
||
- `scanf("%d %d", &a, &b)` → devuelve `2` si leyó los dos.
|
||
- `0` si el primer dato **no coincidía** con el formato (por ejemplo, pedís `%d` y hay `abc`).
|
||
- `EOF` si 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 la `h`.
|
||
- `scanf("%s", word)` → salta el espacio y te da `hola` (se detiene en el espacio).
|
||
- `scanf("%[^\n]", line)` → te da ` hola mundo` **completo** (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 `\n` pendiente y recién ahí lee
|
||
> el carácter. Es el truco para leer un `%c` despué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.
|
||
|
||
```c
|
||
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:
|
||
|
||
```c
|
||
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: en **`scanf`**, para un `double` se usa **`%lf`** (¡no `%f`, como en `printf`!).
|
||
|
||
## 5.6 Falta `%i`, `%u`, `%x`… y `gets` ya no existe
|
||
|
||
- `%i` adivina la base (decimal, `0x` hex, `0` octal); `%d` solo decimal.
|
||
- `%u` es para enteros **sin signo**; `%f` para `float`; `%lf` para `double`.
|
||
- **`gets` fue 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 es `fgets` **con 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`.
|
||
|
||
```c
|
||
#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 `fgets` y después analizar con `sscanf` es más seguro que
|
||
> encadenar `scanf` sobre `stdin`: 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 13.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**
|
||
|
||
```text
|
||
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 13.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**
|
||
|
||
```text
|
||
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 (`%19s` o similar).
|
||
- [ ] Usa `*` para **no** asignar la etiqueta.
|
||
- [ ] Extrae el número correctamente en ambos escenarios.
|
||
- [ ] Comprueba el retorno de `scanf` (`== 1`).
|
||
|
||
### Ejercicio 13.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**
|
||
|
||
```text
|
||
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 sea `3`.
|
||
- [ ] 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 siempre `1`, 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 `á`, `getchar` te va a dar **dos** bytes (`0xC3` y luego `0xA1`). Dos llamadas, **un**
|
||
símbolo.
|
||
- `strlen("á")` da **2**, no `1`.
|
||
- Si recorrés "carácter por carácter" con `getchar`, vas a **partir** los multibyte al medio.
|
||
|
||
Reglas prácticas:
|
||
|
||
1. **Separá las capas.** ¿Querés contar **bytes** o **símbolos**? No es lo mismo.
|
||
2. **Cuando examinés bytes crudos, usá `unsigned char`.** Comparar `0xC3` contra un `char` con signo
|
||
trae resultados confusos.
|
||
3. **Un `char` no alcanza para todo símbolo.** Para eso existen `wchar_t` y `char32_t` (capítulo
|
||
`15-tipos-stddef`).
|
||
4. **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 13.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**
|
||
|
||
```text
|
||
printf 'a\nb\rc\0d' | ./filtro
|
||
a
|
||
<LF>
|
||
b
|
||
<CR>
|
||
c
|
||
<NUL>
|
||
d
|
||
<EOF>
|
||
```
|
||
|
||
> `'\0'` solo aparece si la entrada es un flujo con bytes `0x00` (por ejemplo, una tubería); no se
|
||
> puede teclear en una terminal.
|
||
|
||
**Criterios de aceptación**
|
||
- [ ] Distingue `'\n'`, `'\r'`, `'\0'` y `EOF` correctamente.
|
||
- [ ] Imprime el resto de los caracteres sin alterarlos.
|
||
- [ ] Detecta `'\0'` como byte real `0x00` (no lo confunde con `EOF`).
|
||
- [ ] Termina en el primer `EOF`, con la etiqueta `<EOF>` incluida.
|
||
|
||
### Ejercicio 13.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**
|
||
|
||
```text
|
||
printf 'á' | ./radio
|
||
Bytes: C3 A1
|
||
Total: 2 bytes
|
||
```
|
||
|
||
```text
|
||
printf 'A' | ./radio
|
||
Bytes: 41
|
||
Total: 1 byte
|
||
```
|
||
|
||
> **Lo que tenés que ver:** `á` son **dos** bytes (`C3 A1`), `A` es **uno** (`41`). Ahí está el
|
||
> multibyte, en la mano.
|
||
|
||
**Criterios de aceptación**
|
||
- [ ] Lee con `getchar` en un `int` y corta ante `EOF`.
|
||
- [ ] Muestra cada byte en hexadecimal, con dos dígitos (por ejemplo `0A`, no `A`).
|
||
- [ ] Informa el total de bytes leídos.
|
||
- [ ] El acento da **2** bytes y la `A` da **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:
|
||
|
||
```c
|
||
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:
|
||
|
||
```c
|
||
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`:
|
||
|
||
```c
|
||
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:
|
||
|
||
```c
|
||
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 `scanf` falla 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 `%s` sin ancho, nunca `gets`).
|
||
- **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:
|
||
|
||
```c
|
||
#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 13.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**
|
||
|
||
```text
|
||
Entrada:
|
||
hola
|
||
mundo
|
||
(Ctrl+Z + Enter / Ctrl+D)
|
||
|
||
Salida:
|
||
Leídos 11 caracteres en 2 líneas.
|
||
Fin de la entrada (EOF). Chau.
|
||
```
|
||
|
||
> `hola\n` son 5 caracteres y `mundo\n` son 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 13.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**
|
||
|
||
```text
|
||
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 `fgets` y comprueba el retorno.
|
||
- [ ] Usa `strtol` con 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 13.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:
|
||
|
||
1. Muestre un **prompt** `> ` y lea órdenes del usuario combinando **`fgets`** y **`sscanf`** según el
|
||
comando.
|
||
2. Reconozca los comandos de la tabla de 8.2 (`eco`, `suma`, `salir`).
|
||
3. 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).
|
||
4. **Valide** errores de formato numérico (por ejemplo, `suma 4 abc`) **sin bloquearse**.
|
||
5. Termine de forma limpia al detectar **`EOF`** o al recibir `salir`, **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**
|
||
|
||
```text
|
||
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.
|
||
```
|
||
|
||
```text
|
||
Entrada:
|
||
(Ctrl+Z + Enter / Ctrl+D)
|
||
|
||
stdout:
|
||
> Fin de entrada (EOF). Chau.
|
||
```
|
||
|
||
Caso de orden demasiado larga (con búfer de 128 bytes):
|
||
|
||
```text
|
||
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 `fgets` en un búfer **acotado** y detectar truncamiento (3.4).
|
||
- Para `suma`, interpretar los dos enteros con `sscanf` sobre la línea ya leída (más seguro que
|
||
`scanf` directo sobre `stdin`) 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`: `fgets` devuelve `NULL`.
|
||
- Separar la salida normal (`stdout`) de los errores (`stderr`).
|
||
|
||
**Criterios de aceptación**
|
||
- [ ] Muestra el prompt y lee órdenes con `fgets` acotado.
|
||
- [ ] `eco` imprime el texto conservando los espacios internos.
|
||
- [ ] `suma` con dos enteros válidos imprime la suma correcta.
|
||
- [ ] `suma` con 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.
|
||
- [ ] `salir` y el `EOF` terminan el programa limpiamente (con mensaje de cierre).
|
||
- [ ] No hay bucles infinitos ni comportamiento indefinido ante entradas inesperadas.
|
||
- [ ] Errores por `stderr`, resultados por `stdout`.
|
||
|
||
---
|
||
|
||
## 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).
|
||
- **`EOF` es una señal** (un `int` negativo), no un carácter. Nunca lo guardes en un `char`.
|
||
- **El búfer causa los misterios**: `scanf` deja el `\n` pendiente; por eso existe la limpieza de
|
||
línea, y por eso `fflush(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 `14-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.
|