Apuntes · Capítulo 1Fundamentos

Qué es computar

Qué es computar

Este es el primer capítulo del curso y no supone nada: ni que hayan programado antes, ni que hayan abierto una terminal. Al terminar la semana van a poder hacer tres cosas. Describir un procedimiento con tanta precisión que hasta una máquina pueda seguirlo. Comprobarlo a mano, con papel y lápiz. Y escribir su primer programa en C, compilarlo y ejecutarlo.

La pregunta de fondo es qué significa computar. La respuesta corta: seguir pasos, uno tras otro, sin entender lo que se hace y sin agregar nada de cosecha propia. Una computadora hace solo eso, muy rápido. Todo lo que sabe hacer alguien tuvo que explicárselo antes, sin dejar ningún detalle librado a su criterio. De eso trata este curso.

Lectura. Estos apuntes son el texto del curso: no hay libro ni lecturas externas. Los programas del capítulo están en code/week-01. Más adelante, en este mismo capítulo, verán cómo ejecutarlos y cómo correr sus pruebas con make test.

Ruta de lectura.

  • Antes de clase (lectura obligatoria, 15 a 20 minutos): la introducción, “Qué es un algoritmo”, “Un primer problema: dar vuelto” y “Pseudocódigo”. Es lo mínimo para seguir la clase.
  • En clase: “El curso en una página”, “Seguir un algoritmo a mano”, “Diagramas de flujo”, “De las ideas a la máquina: niveles de lenguaje”, “Del código fuente al ejecutable”, “Anatomía de hola.c” y “Cuando algo sale mal”.
  • Después de clase: para el laboratorio, “La terminal desde cero” y “El laboratorio, paso a paso”; para repasar, “Resumen” y “Ejercicios”. “El código de la semana” explica cómo correr los ejemplos por su cuenta.

El curso en una página

El curso tiene dos sesiones por semana: teoría (2 horas) y laboratorio (2 horas, práctica guiada). Se divide en cuatro partes:

Parte Semanas De qué trata
Fundamentos 1 a 3 Algoritmos, pseudocódigo, variables, operadores, bits, terminal y Git
Programación en C 4 a 7 Decisiones, ciclos, números aleatorios, funciones y pruebas
Memoria y datos 9 a 13 Cómo funciona una computadora, arreglos, cadenas, matrices, struct, archivos, búsqueda y eficiencia
Computación moderna 14 y 15 Una mirada a Python, internet e inteligencia artificial

Cada algoritmo se piensa primero en pseudocódigo y después se escribe en C. Elegimos C porque obliga a entender qué pasa en la memoria de la computadora. No es el lenguaje más cómodo para empezar, pero lo que aprendan con él les va a servir en cualquier otro.

Hay un examen parcial en la semana 8, un examen final en la semana 16 y cuatro prácticas, en las semanas 4, 7, 11 y 14. Desde hoy van a practicar seguir programas a mano. El trabajo es individual: se pueden discutir ideas, pero cada uno escribe su propio código y debe poder explicarlo. El calendario completo y todas las reglas están en el programa del curso.

Qué es un algoritmo

Un algoritmo (algorithm) es una secuencia finita de pasos precisos que resuelve un problema. La palabra viene del nombre de al-Juarismi, un matemático persa del siglo IX.

Ya conocen muchos algoritmos sin haberlos llamado así. Sumar dos números de varias cifras, llevando lo que sobra a la columna siguiente, es uno: si siguen las reglas, el resultado sale bien aunque estén distraídos, y nadie tiene que “inspirarse”. Esa es la idea central. Quien sigue un algoritmo no necesita entender el problema. Solo necesita saber hacer cada paso.

Para que algo sea un algoritmo, debe cumplir tres condiciones:

Condición Qué significa Ejemplo que la rompe
Preciso Cada paso dice exactamente qué hacer “Agrega sal al gusto”
Sin ambigüedad Dos personas que lo sigan hacen lo mismo “Dobla por la esquina de siempre”
Finito Termina después de un número finito de pasos “Lava, enjuaga, repite”

El tercer ejemplo es la instrucción de un frasco de champú. Tomada al pie de la letra, nunca termina: no dice cuántas veces repetir. Una persona lo resuelve sin pensarlo. Una computadora se quedaría lavando para siempre.

Los algoritmos, además, trabajan con datos. Reciben una entrada (el problema concreto) y producen una salida (la respuesta). Un mismo algoritmo sirve para muchas entradas. “Sumar 17 + 25” es un cálculo. “Cómo sumar dos números cualesquiera” es un algoritmo.

Error común. Escribir pasos que una persona entiende por contexto, pero que no dicen exactamente qué hacer. “Dale su vuelto” funciona con un cajero porque el cajero ya sabe cómo se hace. No le dice nada a quien no lo sabe. Para probar un algoritmo, léanlo como si fueran una máquina muy obediente y sin sentido común: si en algún paso tienen que adivinar, ese paso está mal escrito.

Un primer problema: dar vuelto

Trabajan en una bodega. Un cliente debe recibir S/ 7.80 de vuelto y quieren darle la menor cantidad posible de monedas. Para este problema suponemos que hay monedas ilimitadas de estos valores: S/ 5, S/ 2, S/ 1, S/ 0.50, S/ 0.20 y S/ 0.10.

Una persona lo resuelve así: entrega la moneda más grande que no se pase del monto, ve cuánto falta y repite.

  1. Faltan S/ 7.80. La más grande que cabe es S/ 5. Quedan S/ 2.80.
  2. Faltan S/ 2.80. La más grande que cabe es S/ 2. Quedan S/ 0.80.
  3. Faltan S/ 0.80. La de S/ 1 ya no cabe. La más grande que cabe es S/ 0.50. Quedan S/ 0.30.
  4. Faltan S/ 0.30. La más grande que cabe es S/ 0.20. Quedan S/ 0.10.
  5. Faltan S/ 0.10. Cabe la de S/ 0.10. No queda nada.

Son cinco monedas. Ahora hay que escribir esta idea de modo que no dependa de quien la lee. Para eso sirve el pseudocódigo.

Antes, una decisión práctica: vamos a contar en céntimos, no en soles. S/ 7.80 son 780 céntimos y las monedas valen 500, 200, 100, 50, 20 y 10. Así todos los números son enteros y las cuentas son exactas. En la semana 3 verán por qué las computadoras tienen problemas con números como 0.10.

Pseudocódigo

El pseudocódigo (pseudocode) es una forma de escribir un algoritmo sin las reglas de un lenguaje real. No lo ejecuta ninguna máquina: lo leen las personas. Sirve para pensar el algoritmo antes de pelear con el lenguaje de programación.

En este curso siempre lo escribimos con el mismo formato. Este es el algoritmo del vuelto completo:

algoritmo dar_vuelto(vuelto)
    monedas ← 0
    mientras vuelto ≥ 500 hacer
        escribir 500
        vuelto ← vuelto - 500
        monedas ← monedas + 1
    fin mientras
    mientras vuelto ≥ 200 hacer
        escribir 200
        vuelto ← vuelto - 200
        monedas ← monedas + 1
    fin mientras
    mientras vuelto ≥ 100 hacer
        escribir 100
        vuelto ← vuelto - 100
        monedas ← monedas + 1
    fin mientras
    mientras vuelto ≥ 50 hacer
        escribir 50
        vuelto ← vuelto - 50
        monedas ← monedas + 1
    fin mientras
    mientras vuelto ≥ 20 hacer
        escribir 20
        vuelto ← vuelto - 20
        monedas ← monedas + 1
    fin mientras
    mientras vuelto ≥ 10 hacer
        escribir 10
        vuelto ← vuelto - 10
        monedas ← monedas + 1
    fin mientras
    escribir monedas
fin algoritmo

Es largo, pero es el mismo bloque seis veces, uno por cada moneda. Veamos las piezas.

El nombre y los datos. algoritmo dar_vuelto(vuelto) le da un nombre al algoritmo y dice qué dato recibe: el vuelto en céntimos. fin algoritmo marca dónde termina.

Las variables. vuelto y monedas son variables. Una variable es un nombre para un lugar donde se guarda un valor. Imagínenla como una caja con una etiqueta: adentro hay un solo valor a la vez y ese valor puede cambiar. En la semana 2 las verán en C.

La asignación. La flecha ← guarda un valor en una variable. monedas ← 0 se lee “monedas recibe 0”. Primero se calcula lo que está a la derecha y luego se guarda a la izquierda. Por eso monedas ← monedas + 1 tiene sentido: toma el valor actual, le suma 1 y guarda el resultado en la misma caja. El valor anterior se pierde.

Idea clave. ← no es una igualdad matemática. Como ecuación, monedas = monedas + 1 no tendría solución. Como instrucción es perfectamente clara: “aumenta monedas en 1”.

Escribir. escribir 500 muestra el valor al usuario. Aquí significa “entrega una moneda de 500 céntimos”. Existe también leer, que pide un dato al usuario.

Repetir. mientras condición hacer ... fin mientras funciona así:

  1. Se pregunta si la condición es cierta.
  2. Si es cierta, se ejecuta lo que está adentro, una vez, y se vuelve al punto 1.
  3. Si es falsa, se salta a lo que sigue después de fin mientras.

La condición puede ser falsa desde el inicio. En ese caso, lo de adentro no se ejecuta ni una vez.

Decidir. El vuelto no lo necesita, pero otra pieza básica es si. Este algoritmo recibe dos números y escribe el mayor:

algoritmo mayor_de_dos(a, b)
    si a > b entonces
        escribir a
    sino
        escribir b
    fin si
fin algoritmo

Se ejecuta una sola de las dos ramas. Si los números son iguales, a > b es falso y se escribe b, que da lo mismo.

Las reglas del formato son pocas:

Regla Detalle
Palabras clave En español y minúsculas: algoritmo, si, entonces, sino, mientras, hacer, para, desde, hasta, devolver, leer, escribir
Asignar Con ←
Comparar Con =, ≠, <, >, ≤, ≥
Bloques Lo que está adentro lleva una sangría de 4 espacios, y el bloque se cierra con fin si, fin mientras, fin para o fin algoritmo

Las palabras para y devolver aparecerán más adelante, junto con los ciclos y las funciones.

Seguir un algoritmo a mano

Seguir un algoritmo (también se dice hacer su traza) es ejecutarlo ustedes mismos, paso a paso, anotando cómo cambian las variables. Es la herramienta más importante del curso. Sirve para entender un algoritmo ajeno, para encontrar errores en el propio y para responder preguntas de examen.

La técnica es una tabla con una columna por variable. Cada vez que algo cambia, se anota. Sigamos dar_vuelto con vuelto = 780:

Paso Qué se ejecuta ¿Cierto? vuelto monedas Escribe
0 Inicio: monedas ← 0 780 0
1 ¿780 ≥ 500? sí 280 1 500
2 ¿280 ≥ 500? no 280 1
3 ¿280 ≥ 200? sí 80 2 200
4 ¿80 ≥ 200? no 80 2
5 ¿80 ≥ 100? no 80 2
6 ¿80 ≥ 50? sí 30 3 50
7 ¿30 ≥ 50? no 30 3
8 ¿30 ≥ 20? sí 10 4 20
9 ¿10 ≥ 20? no 10 4
10 ¿10 ≥ 10? sí 0 5 10
11 ¿0 ≥ 10? no 0 5
12 escribir monedas 0 5 5

Las monedas son 500, 200, 50, 20 y 10. Suman 780, son cinco y vuelto terminó en 0. Coincide con lo que hicimos de cabeza.

Fíjense en dos detalles de la tabla. En el paso 5 el bloque de la moneda de 100 se salta completo, porque su condición es falsa desde el inicio. Y en los pasos 2, 4, 7, 9 y 11 hay que hacer la pregunta de nuevo aunque ya se sepa la respuesta. La máquina no sabe: pregunta.

Sigamos también mayor_de_dos con dos notas, a = 14 y b = 17:

Paso Qué se ejecuta ¿Cierto? Escribe
1 ¿14 > 17? no
2 Rama sino: escribir b 17

Error común. Olvidar la línea que cambia la condición de un mientras. Sin vuelto ← vuelto - 500, vuelto se queda en 780, la pregunta ¿780 ≥ 500? siempre responde sí y el algoritmo no termina. Es un ciclo infinito. Ya no es un algoritmo, porque no es finito. Cuando escriban un mientras, pregúntense qué línea de adentro acerca la condición a ser falsa.

Antes de confiar: los datos válidos

¿Qué hace dar_vuelto con 785? Sigan la tabla: salen las mismas monedas que con 780, pero vuelto termina en 5, porque ninguna moneda cabe en 5 céntimos. El algoritmo termina, pero no entregó todo el vuelto, y nadie le avisó al usuario.

Esto no es un defecto de la tabla. El algoritmo supone algo sobre su dato: que es un múltiplo de 10 y que no es negativo. Conviene decirlo por escrito. Parte de definir un algoritmo es definir para qué datos funciona.

¿De verdad da la menor cantidad?

Que un método parezca razonable no significa que sea correcto. La idea de tomar siempre la moneda más grande que cabe no funciona con todos los juegos de monedas.

Imaginen un país con monedas de 1, 3 y 4, y un vuelto de 6:

Método Monedas Cantidad
La más grande que cabe 4, 1, 1 3
La mejor posible 3, 3 2

Con esas monedas, el método da un vuelto correcto, pero no el más corto. El problema pedía la menor cantidad, así que ahí el algoritmo no lo resuelve.

Con los valores que elegimos para los soles sí funciona. No lo demostramos en este curso, pero sí lo comprobamos: el archivo test_algorithms.py de la carpeta de la semana calcula, para cada monto de 10 en 10 céntimos hasta S/ 100, la menor cantidad posible probando todas las combinaciones, y la compara con lo que da el método. Son iguales en todos los casos. Eso es una comprobación para esos montos, no una demostración para todos.

Idea clave. Un ejemplo que sale bien no prueba que un algoritmo sea correcto. Un ejemplo que sale mal sí prueba que no lo es.

Diagramas de flujo

Un diagrama de flujo (flowchart) dibuja el mismo algoritmo con formas conectadas por flechas. El camino que siguen las flechas muestra el orden en que se ejecutan los pasos. Es útil para ver de un vistazo dónde se decide y dónde se repite.

Cada forma tiene un significado:

Forma Significado Equivale a
Óvalo Inicio o fin algoritmo, fin algoritmo
Paralelogramo Entrada o salida de datos leer, escribir
Rectángulo Una acción o cálculo monedas ← 0
Rombo Una pregunta con dos salidas, sí y no si, mientras
Flecha Qué paso sigue El orden de las líneas

Tomemos solo el primer bloque del vuelto: contar cuántas monedas de S/ 5 se pueden entregar.

algoritmo monedas_de_cinco(vuelto)
    monedas ← 0
    mientras vuelto ≥ 500 hacer
        vuelto ← vuelto - 500
        monedas ← monedas + 1
    fin mientras
    escribir monedas
fin algoritmo

Este es su diagrama. Cada forma lleva un número para poder nombrarla:

Diagrama de flujo con ocho formas numeradas. De arriba abajo: 1, óvalo Inicio; 2, paralelogramo Recibir vuelto; 3, rectángulo monedas ← 0; 4, rombo con la pregunta vuelto ≥ 500. Si la respuesta es sí, sigue hacia abajo a 5, rectángulo vuelto ← vuelto - 500, y a 6, rectángulo monedas ← monedas + 1, y de ahí una flecha roja da la vuelta por la izquierda y regresa al rombo. Si la respuesta es no, sale a la derecha a 7, paralelogramo Escribir monedas, y termina en 8, óvalo FinDiagrama de flujo con ocho formas numeradas. De arriba abajo: 1, óvalo Inicio; 2, paralelogramo Recibir vuelto; 3, rectángulo monedas ← 0; 4, rombo con la pregunta vuelto ≥ 500. Si la respuesta es sí, sigue hacia abajo a 5, rectángulo vuelto ← vuelto - 500, y a 6, rectángulo monedas ← monedas + 1, y de ahí una flecha roja da la vuelta por la izquierda y regresa al rombo. Si la respuesta es no, sale a la derecha a 7, paralelogramo Escribir monedas, y termina en 8, óvalo Fin
Cada forma dice qué clase de paso es. La flecha que regresa al rombo es lo que hace un ciclo.

La flecha que va de 6 a 4, hacia atrás, es el ciclo: en el dibujo se ve como una vuelta. Un si, en cambio, tiene dos caminos que más abajo se juntan, sin ninguna flecha que regrese.

Si siguen este diagrama con vuelto = 1300, pasan dos veces por las formas 5 y 6, vuelto termina en 300 y escribe 2. Los diagramas sirven bien para algoritmos cortos. Cuando crecen, se vuelven difíciles de dibujar y de modificar, y por eso en el curso usaremos sobre todo pseudocódigo.

De las ideas a la máquina: niveles de lenguaje

La computadora no entiende pseudocódigo. Tampoco entiende C. Su procesador, la CPU, solo ejecuta código máquina: instrucciones muy simples guardadas como números. Cada familia de procesadores tiene el suyo.

Un lenguaje de bajo nivel está cerca de esas instrucciones. Un lenguaje de alto nivel está cerca de cómo piensan las personas, y una sola línea suya equivale a varias instrucciones de la máquina. El nivel es relativo: C es de alto nivel comparado con el código máquina, y de más bajo nivel que Python.

Nivel Ejemplo Quién lo usa
Pseudocódigo escribir "Hola, mundo" Personas, para pensar
Alto nivel (C) printf("Hola, mundo\n"); Personas, para programar
Bajo nivel (ensamblador) bl puts Personas, en casos especiales
Código máquina Números La CPU

El ensamblador es una forma legible del código máquina: una línea por instrucción, con nombres cortos en vez de números. Más adelante van a ver cómo pedirle a gcc que lo muestre. No tienen que aprenderlo en este curso.

Para pasar de C a código máquina se usa un compilador (compiler): un programa que traduce el archivo que escribimos, el código fuente, a un ejecutable, el archivo que la computadora sí puede correr. Nuestro compilador se llama gcc.

Para usar gcc hay que aprender una herramienta nueva: la terminal.

La terminal desde cero

Hasta hoy han usado la computadora con ventanas, íconos y el mouse. Hay otra forma: escribir órdenes. Al principio parece más difícil, pero tiene ventajas. Una orden escrita se puede repetir, copiar y guardar. Además, es la forma normal de usar el compilador, Git y los servidores, así que la van a usar toda su carrera.

Terminal, shell, prompt

Tres palabras que se confunden:

Palabra Qué es
Terminal La ventana donde se escribe y se leen las respuestas
Shell El programa que lee lo que escriben y lo ejecuta. El nuestro se llama bash
Prompt El texto que el shell muestra cuando está listo para recibir una orden

Al abrir la terminal del entorno del curso verán algo parecido a esto:

root@616988be0ce2:/home/ana#

Eso es el prompt. No se escribe: lo pone el shell. Aquí dice tres cosas: el usuario (root, el administrador dentro de este entorno), el nombre de la máquina (616988be0ce2) y la carpeta donde están parados (/home/ana). El de ustedes va a ser distinto, porque depende de la máquina y de la carpeta. En los ejemplos de este capítulo usamos /home/ana como carpeta personal de una estudiante; en el entorno del curso, la carpeta personal es otra.

Como cada prompt es distinto, en este curso lo escribimos siempre como un simple $. Cuando vean esto:

$ pwd
/home/ana

significa: escriban pwd (sin el $), presionen Enter, y la computadora responde /home/ana. Las líneas que empiezan con $ las escriben ustedes. Las demás son la respuesta.

Idea clave. La terminal no hace nada hasta que presionan Enter. Y cuando un comando termina, vuelve a aparecer el prompt. Si el prompt no vuelve, el programa sigue corriendo o está esperando que escriban algo.

Comandos y argumentos

Una orden se llama comando. Tiene un nombre y, a veces, datos adicionales separados por espacios:

$ mkdir lab01

mkdir es el comando y lab01 es su argumento. Algunos argumentos empiezan con un guion y cambian el comportamiento del comando: se llaman opciones. Por ejemplo, ls lista archivos, y ls -l los lista con más detalle.

El espacio separa argumentos, así que importa. mkdir lab01 y mkdirlab01 no son lo mismo. Las mayúsculas también importan: el comando es ls, no Ls.

$ Ls
bash: Ls: command not found

Command not found significa “comando no encontrado”. Los mensajes vienen en inglés, y van a aprender a leerlos sin miedo, porque son lo que más ayuda.

Carpetas y rutas

Los archivos de la computadora se guardan en carpetas, y las carpetas pueden contener otras carpetas. Forman un árbol. El entorno del curso usa Linux, un sistema operativo como Windows o macOS. En Linux, todo cuelga de una carpeta raíz, que se escribe /.

El comando tree dibuja ese árbol. Este es el de una estudiante, Ana, que tiene una carpeta lab01 con un archivo adentro:

$ tree
.
└── lab01
    └── hola.c

2 directories, 1 file

La carpeta de Ana está en /home/ana. Es decir: dentro de la raíz / está home, dentro de home está ana. Ese texto se llama ruta (path), y es la dirección de un archivo o carpeta.

Árbol de carpetas dibujado de arriba abajo: la raíz /, luego home, ana, lab01 y el archivo hola.c. Una nota marca ana como la carpeta donde están. Una flecha roja recorre el árbol desde la raíz hasta hola.c, y es la ruta absoluta /home/ana/lab01/hola.c, que empieza con / y funciona desde cualquier lugar. Una flecha azul lo recorre desde ana hasta hola.c, y es la ruta relativa lab01/hola.c, que no empieza con / y se lee desde la carpeta donde están paradosÁrbol de carpetas dibujado de arriba abajo: la raíz /, luego home, ana, lab01 y el archivo hola.c. Una nota marca ana como la carpeta donde están. Una flecha roja recorre el árbol desde la raíz hasta hola.c, y es la ruta absoluta /home/ana/lab01/hola.c, que empieza con / y funciona desde cualquier lugar. Una flecha azul lo recorre desde ana hasta hola.c, y es la ruta relativa lab01/hola.c, que no empieza con / y se lee desde la carpeta donde están parados
Dos rutas al mismo archivo. Cambia el punto de partida, no el destino.

Hay dos tipos de rutas:

  • Una ruta absoluta empieza con / y se lee desde la raíz: /home/ana/lab01/hola.c. Funciona desde cualquier lugar.
  • Una ruta relativa no empieza con / y se lee desde la carpeta donde están parados: si están en /home/ana, entonces lab01/hola.c es el mismo archivo.

En todo momento, el shell tiene una carpeta actual: la carpeta donde están parados. Y hay tres nombres especiales:

Nombre Significa
. La carpeta actual
.. La carpeta que contiene a la actual (la “de arriba”)
~ Su carpeta personal

Los cinco comandos de hoy

Comando Viene de Qué hace
pwd print working directory Escribe la ruta de la carpeta actual
ls list Lista lo que hay en la carpeta actual
cd ruta change directory Cambia la carpeta actual
mkdir nombre make directory Crea una carpeta
cat archivo concatenate Muestra el contenido de un archivo

Veámoslos en acción. Ana empieza en su carpeta personal, todavía vacía. Las rutas de ustedes serán otras, pero los comandos funcionan igual.

$ pwd
/home/ana
$ ls
$ mkdir lab01
$ ls
lab01
$ cd lab01
$ pwd
/home/ana/lab01
$ ls
$ cd ..
$ pwd
/home/ana

Sigamos la sesión:

Comando Qué pasó
pwd Estamos en /home/ana
ls No escribe nada: la carpeta está vacía
mkdir lab01 No escribe nada: funcionó
ls Ahora aparece lab01
cd lab01 Entramos. No escribe nada, pero la carpeta actual cambió
pwd Lo confirma
ls lab01 está vacía
cd .. Subimos un nivel
pwd Volvimos a /home/ana

Idea clave. En la terminal, el silencio es una buena noticia. Si un comando funciona y no tiene nada que mostrar, no dice nada. Si falla, se queja.

Los .. se pueden encadenar con /. Estando en /home/ana/lab01, cd ../.. sube dos niveles, y cd ~ regresa a la carpeta personal desde donde sea. La carpeta personal de Ana es /home/ana:

$ cd ../..
$ pwd
/home
$ ls
ana
$ cd ~
$ pwd
/home/ana

Equivocarse en la terminal

Equivocarse en la terminal no rompe nada, y el mensaje suele decir qué pasó. Si escriben mal el nombre de una carpeta:

$ cd labo1
bash: cd: labo1: No such file or directory

No such file or directory significa “no existe ese archivo o carpeta”. Revisen con ls qué hay y con pwd dónde están. Casi siempre, o el nombre está mal escrito, o están en otra carpeta.

Tampoco conviene usar espacios en los nombres. Como el espacio separa argumentos, cd recibe aquí dos argumentos, mi y carpeta:

$ cd mi carpeta
bash: cd: too many arguments

Por eso en este curso los nombres van sin espacios ni tildes: lab01, change_780.c.

Error común. Usar cd ~ para volver a la carpeta del curso. En el entorno del curso, el usuario es root y su carpeta personal es /root, que no tiene nada del curso. Para volver, escriban la ruta absoluta de la carpeta del curso. Anótenla la primera vez que abran el entorno: es lo que responde pwd en ese momento.

Error común. Tratar Hola.c y hola.c como si fueran el mismo archivo. En Linux son dos archivos distintos. Si usan Docker en una laptop con Windows o macOS, la carpeta compartida a veces no los distingue, y un programa que funciona allí falla en otro lado. Escriban siempre los nombres exactos.

Tres ayudas que ahorran tiempo

  • Tab completa. Escriban cd la y presionen la tecla Tab: el shell completa lab01 si no hay otra opción. Úsenla siempre: escriben menos y no se equivocan en los nombres.
  • Las flechas recuerdan. La flecha hacia arriba trae el comando anterior. Pueden editarlo y volver a ejecutarlo.
  • Ctrl+C corta. Si un programa se quedó colgado, mantengan Ctrl y presionen C. El prompt vuelve. Si está esperando datos que no piensan escribir, también sirve Ctrl+D, que indica “no hay más datos”. Prueben cat sin argumentos: se queda esperando, y no es que se haya malogrado.

El comando clear limpia la pantalla. Y rm archivo borra un archivo: úsenlo con cuidado, porque la terminal no tiene papelera. Lo borrado no se recupera.

Escribir un archivo

Para programar necesitan un editor de texto, que guarda letras tal cual, sin formato. Word no sirve: guarda mucho más que las letras. Tienen dos caminos, y los dos dejan el mismo archivo.

El editor del entorno. En Codespaces, el panel de arriba es un editor. Pueden abrir un archivo desde la lista de la izquierda o escribiendo en la terminal:

$ code hola.c

Con Docker en su laptop, el entorno corre en un contenedor, una especie de computadora aislada dentro de la suya. Usen el editor que prefieran en la laptop, porque la carpeta es la misma dentro y fuera del contenedor. En ambos casos, guarden con Ctrl+S (o Cmd+S en Mac).

nano, dentro de la terminal. Funciona en cualquier máquina, también en servidores sin ventanas:

$ nano hola.c

Se abre el editor en la misma terminal. Escriban el texto y usen las teclas que aparecen abajo, donde ^ significa Ctrl:

Teclas Qué hace
Ctrl+O y luego Enter Guarda
Ctrl+X Sale

Error común. Cambiar el archivo y no guardarlo. Para gcc solo existe lo que está guardado en el disco. Si corrigieron algo y el error sigue igual, revisen con cat hola.c qué tiene en realidad el archivo.

Del código fuente al ejecutable

Ahora sí. Este es el programa más pequeño que hace algo visible, y el primero que van a escribir. Es el archivo hola.c:

#include <stdio.h>

int main(void) {
    printf("Hola, mundo\n");
    return 0;
}

Después de guardarlo, Ana revisa que esté y lo muestra con cat:

$ ls
hola.c
$ cat hola.c
#include <stdio.h>

int main(void) {
    printf("Hola, mundo\n");
    return 0;
}

Si intenta ejecutarlo todavía no hay nada que ejecutar:

$ ./hola
bash: ./hola: No such file or directory

Hay que compilar:

$ gcc -Wall -Wextra -std=c17 hola.c -o hola
$ ls
hola  hola.c
$ ./hola
Hola, mundo

gcc no dijo nada: buena noticia. Y ahora hay un archivo nuevo, hola. Al ejecutarlo aparece el saludo. Acaban de ver el ciclo básico de programar en C: escribir, compilar, ejecutar.

El comando de compilación tiene varias partes, y conviene entender cada una:

Parte Qué significa
gcc El compilador
-Wall -Wextra Pide que avise de muchas cosas sospechosas (W de warning, advertencia)
-std=c17 Usa la versión del lenguaje que se llama C17, la del curso
hola.c El archivo a traducir
-o hola El nombre del ejecutable que debe crear (o de output, salida)

Si olvidan -o hola, gcc no da error: crea un ejecutable con el nombre a.out.

¿Por qué ./hola y no hola?

$ hola
bash: hola: command not found

Cuando escriben una palabra sola, el shell busca un comando con ese nombre, pero solo en una lista fija de carpetas del sistema, donde viven ls, gcc y los demás. La carpeta actual no está en esa lista, así que no lo encuentra aunque el archivo esté ahí mismo. Si escriben una ruta, el shell va directo a ese archivo. ./hola es la ruta relativa de “el archivo hola que está aquí”, porque . es la carpeta actual.

Por la misma razón, el laboratorio les pide correr env/check.sh: es una ruta, la del archivo check.sh dentro de la carpeta env.

¿Qué archivo ejecuta la computadora?

hola.c es texto para personas. La computadora ejecuta hola. Si intentan ejecutar el código fuente, el shell se niega:

$ ./hola.c
bash: ./hola.c: Permission denied

Permission denied es “permiso denegado”. Los archivos de texto no tienen el permiso de ejecución, y aunque lo tuvieran, la CPU no entiende C. ls -l muestra la diferencia:

$ ls -l
-rwxr-xr-x 1 root root 70432 Nov  2 07:51 hola
-rw-r--r-- 1 root root    82 Nov  2 07:50 hola.c

La x de la primera columna indica que hola se puede ejecutar. El número antes de la fecha es el tamaño en bytes (un byte es la unidad de tamaño de los archivos; la verán en la semana 3). hola.c ocupa 82 bytes. hola ocupa bastante más, porque tiene mucho más que las instrucciones de nuestras pocas líneas. Ese tamaño, y las fechas, cambian de una máquina a otra.

El comando file dice qué tipo de archivo es:

$ file hola
hola: ELF 64-bit LSB pie executable, ARM aarch64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-aarch64.so.1, BuildID[sha1]=5c8baa6788f42a3cd12a18d55af119af9033f3ee, for GNU/Linux 3.7.0, not stripped

No hace falta entenderlo todo. Executable dice que es un ejecutable. ARM aarch64 es el tipo de procesador para el que se compiló esa vez, y en su máquina puede decir otro, como x86-64. Ese es el punto: un ejecutable sirve para una familia de procesadores y un sistema operativo. El código fuente se puede volver a compilar en otra máquina; el ejecutable, en general, no se puede simplemente copiar a cualquier máquina.

Idea clave. hola.c es lo que ustedes escriben y conservan. hola es lo que la computadora ejecuta, y se puede volver a generar en cualquier momento. Si cambian hola.c, hola no se entera: hay que compilar de nuevo.

Qué hace gcc por dentro

“Compilar” es, en realidad, una cadena de cuatro pasos. gcc los hace todos con un solo comando:

Paso Herramienta Qué hace
1 Preprocesador Atiende las líneas con #. Con #include copia el contenido de stdio.h
2 Compilador Traduce C a ensamblador y revisa las reglas del lenguaje
3 Ensamblador Traduce el ensamblador a código máquina
4 Enlazador (linker) Conecta ese código máquina con la biblioteca de C, una colección de funciones ya escritas, como printf, y crea el ejecutable
Cuatro cajas en fila unidas por flechas: Preprocesador, que atiende las líneas con numeral; Compilador, de C a ensamblador; Ensamblador, a código máquina; y Enlazador, que une con la biblioteca. Arriba de la primera entra hola.c, el código fuente. Arriba de la segunda sale, con una flecha punteada, hola.s, y una nota dice que con -S se detiene ahí. Debajo de la cuarta entra la biblioteca de C, con printf y puts, y arriba de ella sale hola, el ejecutableCuatro cajas en fila unidas por flechas: Preprocesador, que atiende las líneas con numeral; Compilador, de C a ensamblador; Ensamblador, a código máquina; y Enlazador, que une con la biblioteca. Arriba de la primera entra hola.c, el código fuente. Arriba de la segunda sale, con una flecha punteada, hola.s, y una nota dice que con -S se detiene ahí. Debajo de la cuarta entra la biblioteca de C, con printf y puts, y arriba de ella sale hola, el ejecutable
Un solo comando, cuatro pasos. El ejecutable sale del último, y por eso un error del enlazador aparece aunque el código compile.

Se puede detener la cadena tras el paso 2 con la opción -S, para mirar el ensamblador:

$ gcc -S hola.c -o hola.s

El archivo hola.s tiene muchas líneas auxiliares que empiezan con punto. Estas son las de la función main, sin esas líneas, tal como salieron en la máquina donde preparamos estos apuntes. Son de un procesador ARM. En una máquina x86-64 serán otras:

main:
	stp	x29, x30, [sp, -16]!
	mov	x29, sp
	adrp	x0, .LC0
	add	x0, x0, :lo12:.LC0
	bl	puts
	mov	w0, 0
	ldp	x29, x30, [sp], 16
	ret

No traten de entenderlas. Solo noten cuánto trabajo le ahorra C: nuestras dos líneas se volvieron ocho instrucciones, y ninguna habla de saludos. También noten que gcc no llamó a printf, sino a puts, otra función de C. Como el texto es fijo y termina en salto de línea, decidió que era equivalente. Un compilador puede cambiar cómo se hace algo si el resultado del programa es el mismo.

Anatomía de hola.c

Línea por línea:

#include <stdio.h>

#include le pide al preprocesador que incluya otro archivo. stdio.h (standard input/output, entrada y salida estándar) es parte de C y le informa al compilador que existe printf y cómo se usa. El código de printf no está ahí: está en la biblioteca de C. El enlazador deja anotado que el programa la necesita, y el sistema la carga cuando el programa empieza a correr. Eso es lo que quiere decir dynamically linked en la salida de file.

int main(void) {

Esto define una función llamada main. Una función es un bloque de código con nombre (la semana 7 trata de ellas). Todo programa en C empieza a ejecutarse en main, así que ese nombre es obligatorio. int dice que devuelve un número entero, y void dice que no recibe datos. La llave { abre el cuerpo de la función.

    printf("Hola, mundo\n");

printf escribe texto en la terminal. Lo que está entre comillas se llama cadena: un texto para el programa, no una orden. \n representa un salto de línea: se escribe con dos caracteres, pero en la salida es uno solo, y hace que lo siguiente empiece en la línea de abajo. El ; termina la instrucción. En C, cada instrucción termina en punto y coma.

    return 0;

return termina main y le entrega un número a quien ejecutó el programa, en este caso el shell. Por convención, 0 significa “todo salió bien” y cualquier otro valor indica algún problema. Se puede ver con echo $?, que escribe el resultado del último comando:

$ ./hola
Hola, mundo
$ echo $?
0

El programa exit_code.c termina con return 7:

$ gcc -Wall -Wextra -std=c17 exit_code.c -o exit_code
$ ./exit_code
Terminó con el código 7
$ echo $?
7
}

La llave cierra el cuerpo de main.

Las instrucciones se ejecutan en orden, de arriba hacia abajo. Si ponemos más printf, se ejecutan uno tras otro. El programa change_780.c escribe la solución que encontramos a mano para el vuelto:

// Escribe las monedas del vuelto de S/ 7.80, encontradas a mano.
// Resuelve un solo caso: el algoritmo general llega en las próximas semanas.
#include <stdio.h>

int main(void) {
    printf("Vuelto de S/ 7.80:\n");
    printf("S/ 5.00\n");
    printf("S/ 2.00\n");
    printf("S/ 0.50\n");
    printf("S/ 0.20\n");
    printf("S/ 0.10\n");
    printf("Total: 5 monedas\n");
    return 0;
}
$ gcc -Wall -Wextra -std=c17 change_780.c -o change_780
$ ./change_780
Vuelto de S/ 7.80:
S/ 5.00
S/ 2.00
S/ 0.50
S/ 0.20
S/ 0.10
Total: 5 monedas

Las líneas que empiezan con // son comentarios: notas para quien lee, que el compilador ignora. La sangría de 4 espacios tampoco le importa al compilador, pero sin ella el código es difícil de leer, así que en este curso es obligatoria.

Miren bien este programa, porque no es el algoritmo del vuelto. Solo recita una respuesta calculada a mano. Si el vuelto fuera S/ 3.40, habría que reescribirlo. Para que el programa calcule el vuelto por sí mismo le falta guardar valores y repetir. Eso llega en las semanas 2 y 5.

En C. Un programa en C tiene esta estructura: #include arriba, luego main con sus instrucciones entre llaves, cada una terminada en ;, y al final return 0;. Por ahora, todos sus programas tendrán esta forma.

Cuando algo sale mal

Van a tener errores todo el semestre, igual que cualquier persona que programa. La diferencia entre sufrirlos y resolverlos es saber leerlos. Hay dos grandes tipos:

Tipo Cuándo aparece ¿Hay ejecutable?
Error de compilación Al correr gcc No
Error de ejecución Al correr el programa Sí, se compiló bien

Errores de compilación

El programa rompe las reglas de C, y gcc se niega a traducirlo. Son los más fáciles, porque el compilador dice dónde está el problema.

Le quitamos el ; a la línea de printf:

$ gcc -Wall -Wextra -std=c17 hola.c -o hola
hola.c: In function ‘main’:
hola.c:4:28: error: expected ‘;’ before ‘return’
    4 |     printf("Hola, mundo\n")
      |                            ^
      |                            ;
    5 |     return 0;
      |     ~~~~~~

La línea importante es la que tiene error:, y se lee por partes:

Parte Significa
hola.c El archivo
4 La línea
28 La columna, contando caracteres desde la izquierda
error Es un error: no hay ejecutable
expected ‘;’ before ‘return’ “Se esperaba ; antes de return”

Debajo, gcc copia la línea, marca el lugar con ^ y hasta sugiere qué poner. Noten que menciona return, que está en la línea 5: encontró esa palabra cuando todavía esperaba el ;. Por eso nombra la línea siguiente, aunque el ; falta al final de la línea 4.

Ahora dejamos el ; y le quitamos la comilla que cierra el texto:

$ gcc -Wall -Wextra -std=c17 hola.c -o hola
hola.c: In function ‘main’:
hola.c:4:12: warning: missing terminating " character
    4 |     printf("Hola, mundo\n);
      |            ^
hola.c:4:12: error: missing terminating " character
    4 |     printf("Hola, mundo\n);
      |            ^~~~~~~~~~~~~~~~
hola.c:5:5: error: expected expression before ‘return’
    5 |     return 0;
      |     ^~~~~~
hola.c:5:14: error: expected ‘;’ before ‘}’ token
    5 |     return 0;
      |              ^
      |              ;
    6 | }
      | ~

¡Tres errores por una sola comilla! Es normal. Sin la comilla final, el ) y el ; quedan como parte de un texto que nunca termina, y gcc pierde el hilo: los errores de la línea 5 son consecuencia del primero. La línea 5 está bien. Si corrigen la línea 4, todos desaparecen.

Idea clave. Lean siempre el primer error, corríjanlo y vuelvan a compilar. Los que vienen después pueden ser consecuencia de ese. Y si la línea que señala gcc parece correcta, revisen también la anterior.

Si escriben mal el nombre de una función, gcc lo detecta y hasta propone un arreglo:

$ gcc -Wall -Wextra -std=c17 hola.c -o hola
hola.c: In function ‘main’:
hola.c:4:5: error: implicit declaration of function ‘print’; did you mean ‘printf’? [-Wimplicit-function-declaration]
    4 |     print("Hola, mundo\n");
      |     ^~~~~
      |     printf

Implicit declaration quiere decir que se está usando una función que nadie le presentó. Lo mismo pasa si olvidan el #include, y gcc les dice qué línea falta:

$ gcc -Wall -Wextra -std=c17 hola.c -o hola
hola.c: In function ‘main’:
hola.c:2:5: error: implicit declaration of function ‘printf’ [-Wimplicit-function-declaration]
    2 |     printf("Hola, mundo\n");
      |     ^~~~~~
hola.c:1:1: note: include ‘<stdio.h>’ or provide a declaration of ‘printf’
  +++ |+#include <stdio.h>
    1 | int main(void) {
hola.c:2:5: warning: incompatible implicit declaration of built-in function ‘printf’ [-Wbuiltin-declaration-mismatch]
    2 |     printf("Hola, mundo\n");
      |     ^~~~~~
hola.c:2:5: note: include ‘<stdio.h>’ or provide a declaration of ‘printf’

Estos mensajes dependen de la versión de gcc. Los de estos apuntes salen de la versión del entorno del curso, la 14.2. En versiones antiguas, algunos de estos errores eran solo advertencias.

Un último caso. Si escriben Main con mayúscula, el mensaje se ve muy distinto:

$ gcc -Wall -Wextra -std=c17 hola.c -o hola
/usr/bin/ld: /usr/lib/gcc/aarch64-linux-gnu/14/../../../aarch64-linux-gnu/Scrt1.o: in function `_start':
(.text+0x1c): undefined reference to `main'
/usr/bin/ld: (.text+0x20): undefined reference to `main'
collect2: error: ld returned 1 exit status

Para C, Main es una función cualquiera, así que el código es válido. El que se queja es ld, el enlazador (paso 4): busca dónde empieza el programa y no encuentra ningún main. Las rutas del mensaje dependen de la máquina. Lo que importa es *undefined reference to main'*, "no hay ninguna definición de main`”.

Error común. Compilar con errores y luego ejecutar sin mirar. Cuando gcc encuentra un error en el código, como un ; que falta, no toca el ejecutable anterior. Si ya existía un hola de antes, sigue ahí, y ./hola ejecuta el programa viejo:

$ gcc -Wall -Wextra -std=c17 hola.c -o hola
hola.c: In function ‘main’:
hola.c:4:28: error: expected ‘;’ before ‘return’
    4 |     printf("Hola, mundo\n")
      |                            ^
      |                            ;
    5 |     return 0;
      |     ~~~~~~
$ ls
hola  hola.c
$ ./hola
Hola, mundo

Parece que el programa funciona, pero es el de la vez anterior. Lean siempre lo que dice gcc antes de ejecutar. Los errores del enlazador, como el de Main, son distintos: ahí gcc sí borra el ejecutable anterior.

Advertencias

Una advertencia (warning) no detiene la compilación: el ejecutable se crea. Pero gcc les está diciendo que algo es sospechoso. En este curso el código no debe dar advertencias. Tómenlas como errores que todavía no los han mordido.

Errores de ejecución

Aquí el programa compiló, pero falla o se porta mal al correr. Son más difíciles, porque nadie les señala una línea: puede que ni siquiera haya mensaje.

Resultado incorrecto. El programa errors/wrong_change.c está bien escrito para gcc, que no dice nada. Corre y termina normalmente:

$ gcc -Wall -Wextra -std=c17 wrong_change.c -o wrong_change
$ ./wrong_change
Vuelto de S/ 7.80:
S/ 5.00
S/ 2.00
S/ 1.00
S/ 0.50
S/ 0.20
S/ 0.10
Total: 6 monedas

Esas monedas suman S/ 8.80. El cliente recibe un sol de más, y solo se nota si alguien compara el resultado con lo que debía salir. Por eso existen las pruebas, y por eso siguen sus algoritmos a mano.

Salida con otro aspecto. Este es un error pequeño y muy común. Sin el \n, el saludo queda pegado al prompt. Lo siguiente (pwd) es lo que se escribió después:

$ ./hola
Hola, mundo$ pwd
/home/ana/lab01

El programa hace algo inválido. El programa errors/divide_by_zero.c intenta dividir entre cero. El %d de printf significa “aquí va un número entero”. Lo verán bien la próxima semana; por ahora, cópienlo tal cual.

// Dividir entre cero: gcc avisa, pero igual crea el ejecutable.
#include <stdio.h>

int main(void) {
    printf("Antes de dividir\n");
    printf("%d\n", 10 / 0);
    printf("Después de dividir\n");
    return 0;
}

Primero, sin ninguna ayuda adicional:

$ gcc -Wall -Wextra -std=c17 divide_by_zero.c -o divide_by_zero
divide_by_zero.c: In function ‘main’:
divide_by_zero.c:6:23: warning: division by zero [-Wdiv-by-zero]
    6 |     printf("%d\n", 10 / 0);
      |                       ^
$ ./divide_by_zero
Antes de dividir
0
Después de dividir

gcc avisó y creó el ejecutable. En la máquina donde preparamos estos apuntes, escribe un 0 y sigue, como si nada. Ese 0 no significa nada: C no define qué pasa al dividir entre cero, así que cualquier resultado es posible. En un procesador de otro tipo, el programa puede terminar de golpe en esa línea, sin llegar a la última. Un programa con este error no es confiable, aunque hoy parezca funcionar.

Por eso en los laboratorios se compila además con -g -fsanitize=address,undefined. -fsanitize agrega controles que vigilan el programa mientras corre, y -g guarda datos para que los mensajes puedan indicar el archivo y la línea:

$ gcc -Wall -Wextra -std=c17 -g -fsanitize=address,undefined divide_by_zero.c -o divide_by_zero
divide_by_zero.c: In function ‘main’:
divide_by_zero.c:6:23: warning: division by zero [-Wdiv-by-zero]
    6 |     printf("%d\n", 10 / 0);
      |                       ^
$ ./divide_by_zero
Antes de dividir
divide_by_zero.c:6:23: runtime error: division by zero
0
Después de dividir

Runtime error es “error de ejecución”, y trae archivo, línea y columna. Cuando vean cómo se dañan cosas en la memoria, en las semanas 9 y 10, estas opciones les van a ahorrar horas.

El laboratorio, paso a paso

Los enunciados están en la guía de la semana. Aquí van las ideas que necesitan para cada punto.

Punto 1: abrir el entorno

Todo el curso se trabaja en un mismo entorno: una máquina Linux con gcc, Git y todo lo demás ya instalado. Lo normal es abrirlo en el navegador con GitHub Codespaces; quien tenga laptop también puede usarlo con Docker. Los pasos están en labs/README.md.

Una vez abierto, comprueben que todo funciona, desde la carpeta principal del curso:

$ env/check.sh
ok    gcc compila C17 sin advertencias
ok    el programa en C se ejecuta
ok    el sanitizer compila
ok    el sanitizer detecta un error de memoria
ok    make
ok    gdb
ok    git
ok    Python 3.14

Si alguna línea dice FALLA, avisen al docente.

Punto 2: moverse por las carpetas

Su trabajo de esta semana va en code/week-01/lab. Desde la carpeta principal:

$ cd code/week-01/lab
$ ls
change.txt  compiler_errors.txt
$ mkdir lab01
$ cd lab01
$ pwd

pwd les mostrará una ruta que termina en code/week-01/lab/lab01. El inicio depende de dónde abrieron el entorno.

Punto 3: el primer programa

Escriban hola.c dentro de lab01, con cualquiera de los editores. Escríbanlo a mano, sin copiar y pegar: los dedos también aprenden. Luego compilen y ejecuten con los comandos que vieron arriba. Debe escribir exactamente Hola, mundo.

Punto 4: romperlo a propósito

Borren el ; de printf, compilen y lean. Vuelvan a ponerlo. Luego borren la comilla que cierra el texto, compilen y lean. Anoten en lab/compiler_errors.txt en qué línea avisa gcc y qué dice. Abran ese archivo con el editor y cambien cada ___ por su respuesta. Al final, dejen hola.c correcto.

Punto 5: el vuelto a mano

En lab/change.txt está el inicio del algoritmo. Ahí mismo están los símbolos ← y ≥ para copiarlos. Complétenlo, sin mirar estos apuntes si pueden. Luego llenen la tabla para vuelto = 780, con una fila por moneda.

Comprobar con make

Escribir el comando de gcc cada vez se vuelve pesado, y más cuando un proyecto tiene muchos archivos. make es una herramienta que lee un archivo llamado Makefile, donde están escritas las recetas, y ejecuta los comandos necesarios. Cada carpeta de código del curso tiene el suyo. Desde code/week-01:

Comando Qué hace
make Compila los programas de la clase y deja los ejecutables en build/
make test Corre las pruebas de la semana
make test-lab Revisa su laboratorio
make clean Borra lo generado

Por ejemplo:

$ make
mkdir -p build
gcc -Wall -Wextra -std=c17 -g -fsanitize=address,undefined hola.c -o build/hola
gcc -Wall -Wextra -std=c17 -g -fsanitize=address,undefined change_780.c -o build/change_780
gcc -Wall -Wextra -std=c17 -g -fsanitize=address,undefined exit_code.c -o build/exit_code
$ build/hola
Hola, mundo

Una prueba (test) es un programa que comprueba que otro hace lo que debe. Las de make test-lab compilan su hola.c sin permitir advertencias, revisan lo que escribe y miran sus dos archivos de texto. Están hechas para fallar hasta que terminen. Antes de empezar, este es el inicio de lo que se ve:

$ make test-lab
LAB_DIR=lab python3 -m unittest test_lab
FFFF
======================================================================
FAIL: test_folder_exists (test_lab.HolaTest.test_folder_exists)
----------------------------------------------------------------------
AssertionError: No existe la carpeta lab/lab01. Créenla con mkdir.

Cada F es una prueba que falló, y debajo está el motivo. Cuando todo esté bien, verán esto, con otro tiempo porque depende de la máquina:

$ make test-lab
LAB_DIR=lab python3 -m unittest test_lab
....
----------------------------------------------------------------------
Ran 4 tests in 0.023s

OK

El código de la semana

Todo está en code/week-01: los programas del capítulo, la carpeta errors/ con los programas que tienen errores a propósito, la carpeta lab/ con los archivos del laboratorio, y las pruebas, escritas en Python, en los archivos test_examples.py, test_algorithms.py y test_lab.py.

No hace falta que entiendan las pruebas por ahora. Pero noten qué hacen: comprueban lo que dicen estos apuntes. Los mensajes de gcc de este capítulo, por ejemplo, se comparan con lo que gcc escribe de verdad. make test debe terminar así, con un tiempo que depende de la máquina:

$ make test
mkdir -p build
gcc -Wall -Wextra -std=c17 -g -fsanitize=address,undefined hola.c -o build/hola
gcc -Wall -Wextra -std=c17 -g -fsanitize=address,undefined change_780.c -o build/change_780
gcc -Wall -Wextra -std=c17 -g -fsanitize=address,undefined exit_code.c -o build/exit_code
python3 -m unittest test_examples test_algorithms
............................
----------------------------------------------------------------------
Ran 28 tests in 1.270s

OK

Resumen

  • Computar es seguir pasos sin agregar criterio propio.
  • Un algoritmo es una secuencia finita de pasos precisos y sin ambigüedad, que recibe datos y produce un resultado.
  • El pseudocódigo describe un algoritmo para personas. ← asigna, mientras repite y si decide. Un diagrama de flujo dibuja lo mismo.
  • Seguir un algoritmo a mano con una tabla de variables es la forma de entenderlo y de encontrarle errores. Un ejemplo que sale bien no prueba que sea correcto.
  • Una terminal es la ventana; el shell ejecuta los comandos; el prompt indica que está listo. pwd, ls, cd, mkdir y cat bastan para moverse. Una ruta absoluta empieza con /, una relativa parte de la carpeta actual, y .. es la carpeta de arriba.
  • Un compilador traduce el código fuente a un ejecutable. La CPU solo ejecuta código máquina, y este depende del procesador.
  • El ciclo de trabajo en C es escribir, compilar y ejecutar: gcc -Wall -Wextra -std=c17 hola.c -o hola y luego ./hola.
  • Todo programa en C empieza en main, y sus instrucciones se ejecutan en orden y terminan en ;. return 0 significa que todo salió bien.
  • Un error de compilación impide crear el ejecutable. Lean el primero, corríjanlo y compilen de nuevo. Un error de ejecución aparece al correr el programa, y a veces sin ningún mensaje.

Ejercicios

  1. ¿Cuáles de estas instrucciones son un algoritmo? Para las que no lo son, expliquen por qué, usando las condiciones de esta semana. a) “Ordena estos números.” b) “Sal de casa temprano.” c) “Empieza en 1. Escribe el número y súmale 2. Repite para siempre.” d) “Escribe 1. Escribe 2. Escribe 3. Termina.”

  2. Reescriban como pasos precisos las instrucciones para prender una cocina a gas y hervir agua. Pásenselas a otra persona: que busque un paso en el que deba adivinar.

  3. Sigan dar_vuelto a mano con vuelto = 1360, con una tabla como la del capítulo. ¿Qué monedas salen y cuántas son?

  4. Sigan dar_vuelto con vuelto = 785. ¿Qué valor tiene vuelto al terminar? Agreguen al algoritmo un si al final que escriba falta entregar si quedó algo sin entregar.

  5. Escriban en pseudocódigo un algoritmo que reciba tres números y escriba el mayor. Síganlo a mano con 4, 9, 2. Luego pruébenlo con 9, 9, 2 y con 2, 2, 2.

  6. Dibujen el diagrama de flujo de mayor_de_dos. ¿Tiene alguna flecha hacia atrás? ¿Por qué?

  7. Un país tiene monedas de 1, 5 y 7. Sigan el método de “la más grande que cabe” para un vuelto de 10. Luego encuentren una forma de darlo con menos monedas. ¿Qué concluyen sobre este método?

  8. Estando en /home/ana/lab01, ¿a qué carpeta llevan cd .. y cd ../..? ¿Y cd /? ¿Y cd ~, si la carpeta personal es /home/ana? Respondan primero y luego comprueben con carpetas propias usando mkdir, cd y pwd.

  9. ¿Qué archivo ejecuta la computadora: hola.c o hola? ¿Por qué? ¿Qué pasa con hola si cambian hola.c y no compilan de nuevo? Compruébenlo.

  10. Sin compilar, predigan para cada cambio: ¿compila?, si compila, ¿qué escribe?, ¿qué responde echo $? después de ejecutarlo? Luego compruébenlo. Todos parten de hola.c. a) Cambian printf por Printf. b) Cambian "Hola, mundo\n" por "Hola, mundo". c) Borran la llave } final. d) Escriben todo el cuerpo de main en una sola línea. e) Cambian return 0; por return 3;.

  11. Escriban un programa receipt.c que escriba una boleta de una bodega con tres productos y sus precios, un producto por línea, y el total. Los montos los calculan ustedes a mano. Compílenlo sin advertencias. ¿Qué tendrían que cambiar si el cliente compra otro producto? ¿Por qué esto todavía no es un algoritmo para calcular el total?

  12. Investiguen con la terminal, sin buscar afuera: creen la carpeta prueba, entren, y compilen su hola.c sin la opción -o, indicando su ruta (por ejemplo, gcc ../hola.c). ¿Cómo se llama el ejecutable? ¿Cómo lo ejecutan? ¿Qué dice echo $? justo después de ejecutarlo y justo después de un comando que falla, como cd no_existe?