Semana 1 — Qué es computar
Objetivos
- Explicar qué es un algoritmo y escribir uno en pseudocódigo y en un diagrama de flujo.
- Seguir un algoritmo a mano con una tabla de variables.
- Explicar qué hace un compilador.
- Abrir el entorno del curso, usar la terminal, y compilar y ejecutar un primer programa en C.
Teoría — Presentación · Algoritmos y pseudocódigo · Del programa al ejecutable
Lectura: Apuntes, semana 1.
Temas:
- Cómo funciona el curso: partes, laboratorios, prácticas y exámenes.
- Algoritmo: pasos precisos, finitos y sin ambigüedad. Ejemplo: dar vuelto en soles con la menor cantidad de monedas.
- Pseudocódigo y diagramas de flujo. Seguir un algoritmo a mano.
- Error común: escribir pasos que una persona entiende por contexto, pero que no dicen exactamente qué hacer.
- Lenguajes de alto y bajo nivel. Del código fuente al ejecutable con
gcc. - Anatomía de
hola.c:#include,main,printfyreturn 0. Errores de compilación y de ejecución.
Laboratorio — Entorno, terminal y primer programa
- Abrir el entorno siguiendo
labs/README.mdy correrenv/check.sh. Todas las líneas deben decirok. - Usar la terminal:
pwd,ls,cd,mkdirycat. Crear la carpetalab01. - Escribir
hola.c, compilarlo congcc -Wall -Wextra -std=c17 hola.c -o holay ejecutarlo con./hola. - Borrar a propósito un
;y luego una comilla. Anotar en qué línea avisagccy qué dice. - Escribir en pseudocódigo el algoritmo del vuelto para 7.80 soles y seguirlo a mano.
Preguntas de repaso
- ¿Qué le falta a la instrucción “ordena estos números” para ser un algoritmo?
- Escriban en pseudocódigo un algoritmo que reciba tres números y escriba el mayor. Síganlo a mano con
4, 9, 2. - ¿Qué archivo ejecuta la computadora:
hola.cohola? ¿Por qué? - ¿Cuál es la diferencia entre un error de compilación y uno de ejecución?
- Estando en
/home/ana/lab01, ¿a qué carpeta llevancd ..ycd ../..?
Código de la semana
Compilar y probar dentro del contenedor del curso con make y make test.
Ejemplos (3)Errores (8)Pruebas (3)Laboratorio (2)Compilación (1)
Ejemplos
Los programas que usan los apuntes y la clase.
// 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;
}// Termina con el código de salida 7 para verlo con `echo $?`.
#include <stdio.h>
int main(void) {
printf("Terminó con el código 7\n");
return 7;
}#include <stdio.h>
int main(void) {
printf("Hola, mundo\n");
return 0;
}Errores
Ejemplos con errores a propósito: no compilan, fallan o dan un resultado incorrecto. Los apuntes explican cada uno.
// 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;
}int main(void) {
printf("Hola, mundo\n");
return 0;
}#include <stdio.h>
int main(void) {
printf("Hola, mundo\n);
return 0;
}#include <stdio.h>
int main(void) {
printf("Hola, mundo\n")
return 0;
}#include <stdio.h>
int Main(void) {
printf("Hola, mundo\n");
return 0;
}#include <stdio.h>
int main(void) {
print("Hola, mundo\n");
return 0;
}#include <stdio.h>
int main(void) {
printf("Hola, mundo");
return 0;
}// Compila y corre sin quejas, pero el resultado está mal: suman S/ 8.80.
#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/ 1.00\n");
printf("S/ 0.50\n");
printf("S/ 0.20\n");
printf("S/ 0.10\n");
printf("Total: 6 monedas\n");
return 0;
}Pruebas
Comprueban lo que afirman los apuntes; se ejecutan con make test.
"""Comprueba los algoritmos que el capítulo sigue a mano. Cada función copia, paso
a paso, un pseudocódigo de los apuntes: give_change es dar_vuelto, five_sol_coins es
monedas_de_cinco y larger_of_two es mayor_de_dos."""
import unittest
COINS = (500, 200, 100, 50, 20, 10)
def give_change(amount):
"""Devuelve las monedas, cada valor restante y lo que queda al final."""
coins, remaining = [], []
for coin in COINS:
while amount >= coin:
amount -= coin
coins.append(coin)
remaining.append(amount)
return coins, remaining, amount
def five_sol_coins(amount):
count = 0
while amount >= 500:
amount -= 500
count += 1
return count, amount
def larger_of_two(a, b):
if a > b:
return a
return b
def greedy_count(amount, coins):
count = 0
for coin in sorted(coins, reverse=True):
while amount >= coin:
amount -= coin
count += 1
return count
def fewest_count(amount, coins):
"""Menor cantidad de monedas, probando todas las combinaciones posibles."""
best = [0] + [None] * amount
for value in range(1, amount + 1):
options = [best[value - c] for c in coins
if c <= value and best[value - c] is not None]
best[value] = min(options) + 1 if options else None
return best[amount]
def final_grade(ep, ef, practicas):
pp = sum(sorted(practicas, reverse=True)[:3]) / 3
return pp, (ep + 2 * ef + pp) / 4
class ChangeTest(unittest.TestCase):
def test_780_table(self):
coins, remaining, left = give_change(780)
self.assertEqual(coins, [500, 200, 50, 20, 10])
self.assertEqual(remaining, [280, 80, 30, 10, 0])
self.assertEqual(left, 0)
def test_1360(self):
coins, _, left = give_change(1360)
self.assertEqual(coins, [500, 500, 200, 100, 50, 10])
self.assertEqual(left, 0)
def test_785_leaves_five(self):
coins, _, left = give_change(785)
self.assertEqual(coins, [500, 200, 50, 20, 10])
self.assertEqual(left, 5)
def test_zero_gives_no_coins(self):
self.assertEqual(give_change(0), ([], [], 0))
def test_greedy_is_fewest_for_these_coins(self):
# Todos los montos hasta S/ 100, de 10 en 10 céntimos.
for amount in range(0, 10001, 10):
self.assertEqual(greedy_count(amount, COINS),
fewest_count(amount, COINS), amount)
def test_greedy_is_not_always_fewest(self):
self.assertEqual(greedy_count(6, (1, 3, 4)), 3)
self.assertEqual(fewest_count(6, (1, 3, 4)), 2)
# Ejercicio 7 de los apuntes.
self.assertEqual(greedy_count(10, (1, 5, 7)), 4)
self.assertEqual(fewest_count(10, (1, 5, 7)), 2)
class SmallAlgorithmsTest(unittest.TestCase):
def test_five_sol_coins(self):
self.assertEqual(five_sol_coins(1300), (2, 300))
self.assertEqual(five_sol_coins(780), (1, 280))
self.assertEqual(five_sol_coins(300), (0, 300))
def test_larger_of_two(self):
self.assertEqual(larger_of_two(14, 17), 17)
self.assertEqual(larger_of_two(17, 14), 17)
self.assertEqual(larger_of_two(9, 9), 9)
def test_grade_example(self):
pp, final = final_grade(12, 14, (11, 15, 13, 9))
self.assertEqual(pp, 13)
self.assertEqual(final, 13.25)
if __name__ == "__main__":
unittest.main()"""Comprueba lo que los apuntes afirman de los programas: lo que escriben, lo que
dice gcc y cómo responde la terminal. Las líneas de gcc se buscan también en
notes/week-01.md, así que un cambio en el código o en los apuntes rompe la prueba."""
import shutil
import subprocess
import tempfile
import unittest
from pathlib import Path
HERE = Path(__file__).resolve().parent
NOTES = HERE.parent.parent / "notes" / "week-01.md"
FLAGS = ["-Wall", "-Wextra", "-std=c17"]
SANITIZE = ["-g", "-fsanitize=address,undefined"]
def compile_as(source, name, cwd, extra=()):
"""Copia el programa con otro nombre, como quien edita hola.c, y lo compila."""
shutil.copy(HERE / source, Path(cwd) / f"{name}.c")
return subprocess.run(["gcc", *FLAGS, *extra, f"{name}.c", "-o", name],
cwd=cwd, capture_output=True, text=True)
def run(cwd, name):
return subprocess.run([f"./{name}"], cwd=cwd, capture_output=True, text=True, timeout=5)
def shell(script, cwd):
return subprocess.run(["bash", "-c", script], cwd=cwd, capture_output=True, text=True)
class ProgramsTest(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.addCleanup(self.tmp.cleanup)
self.dir = self.tmp.name
def test_programs_compile_without_warnings(self):
for source in ("hola", "change_780", "exit_code"):
result = compile_as(f"{source}.c", source, self.dir, ["-Werror"])
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(result.stderr, "")
def test_hola(self):
compile_as("hola.c", "hola", self.dir)
result = run(self.dir, "hola")
self.assertEqual(result.stdout, "Hola, mundo\n")
self.assertEqual(result.returncode, 0)
def test_change_780_adds_up(self):
compile_as("change_780.c", "change", self.dir)
lines = run(self.dir, "change").stdout.splitlines()
coins = [round(float(line.removeprefix("S/ ")) * 100) for line in lines[1:-1]]
self.assertEqual(coins, [500, 200, 50, 20, 10])
self.assertEqual(sum(coins), 780)
self.assertEqual(lines[-1], "Total: 5 monedas")
def test_wrong_change_compiles_but_is_wrong(self):
result = compile_as("errors/wrong_change.c", "wrong", self.dir)
self.assertEqual((result.returncode, result.stderr), (0, ""))
lines = run(self.dir, "wrong").stdout.splitlines()
coins = [round(float(line.removeprefix("S/ ")) * 100) for line in lines[1:-1]]
self.assertEqual(sum(coins), 880)
def test_exit_code(self):
compile_as("exit_code.c", "exit_code", self.dir)
result = shell("./exit_code > /dev/null; echo $?", self.dir)
self.assertEqual(result.stdout, "7\n")
def test_no_newline(self):
compile_as("errors/no_newline.c", "hola", self.dir)
self.assertEqual(run(self.dir, "hola").stdout, "Hola, mundo")
def test_failed_compilation_keeps_old_executable(self):
compile_as("hola.c", "hola", self.dir)
result = compile_as("errors/missing_semicolon.c", "hola", self.dir)
self.assertNotEqual(result.returncode, 0)
self.assertEqual(run(self.dir, "hola").stdout, "Hola, mundo\n")
def test_link_error_removes_old_executable(self):
compile_as("hola.c", "hola", self.dir)
result = compile_as("errors/misspelled_main.c", "hola", self.dir)
self.assertNotEqual(result.returncode, 0)
self.assertFalse((Path(self.dir) / "hola").exists())
def test_failed_compilation_creates_nothing(self):
result = compile_as("errors/missing_quote.c", "hola", self.dir)
self.assertNotEqual(result.returncode, 0)
self.assertFalse((Path(self.dir) / "hola").exists())
def test_divide_by_zero_warns_and_reports_when_run(self):
result = compile_as("errors/divide_by_zero.c", "divide_by_zero", self.dir, SANITIZE)
self.assertEqual(result.returncode, 0)
self.assertIn("warning: division by zero", result.stderr)
report = run(self.dir, "divide_by_zero")
self.assertIn("divide_by_zero.c:6:23: runtime error: division by zero", report.stderr)
class CompilerMessagesTest(unittest.TestCase):
"""Cada línea debe salir de gcc y estar copiada tal cual en los apuntes."""
CASES = {
"errors/missing_semicolon.c": ["hola.c:4:28: error: expected ‘;’ before ‘return’"],
"errors/missing_quote.c": [
'hola.c:4:12: warning: missing terminating " character',
'hola.c:4:12: error: missing terminating " character',
"hola.c:5:5: error: expected expression before ‘return’",
"hola.c:5:14: error: expected ‘;’ before ‘}’ token",
],
"errors/misspelled_printf.c": [
"hola.c:4:5: error: implicit declaration of function ‘print’; "
"did you mean ‘printf’? [-Wimplicit-function-declaration]",
],
"errors/missing_include.c": [
"hola.c:2:5: error: implicit declaration of function ‘printf’ "
"[-Wimplicit-function-declaration]",
"hola.c:1:1: note: include ‘<stdio.h>’ or provide a declaration of ‘printf’",
],
"errors/misspelled_main.c": ["undefined reference to `main'"],
}
def test_messages(self):
notes = NOTES.read_text(encoding="utf-8")
for source, lines in self.CASES.items():
with self.subTest(source), tempfile.TemporaryDirectory() as tmp:
result = compile_as(source, "hola", tmp)
self.assertNotEqual(result.returncode, 0)
self.assertFalse((Path(tmp) / "hola").exists())
for line in lines:
self.assertIn(line, result.stderr)
self.assertIn(line, notes)
class TerminalTest(unittest.TestCase):
"""Los comandos de la sección de la terminal, en una carpeta temporal."""
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.addCleanup(self.tmp.cleanup)
self.dir = Path(self.tmp.name).resolve()
def test_mkdir_ls_cd_pwd(self):
result = shell("ls; mkdir lab01; ls; cd lab01; pwd; ls; cd ..; pwd", self.dir)
self.assertEqual(result.stdout, f"lab01\n{self.dir}/lab01\n{self.dir}\n")
def test_cd_dot_dot_twice(self):
(self.dir / "ana" / "lab01").mkdir(parents=True)
result = shell("cd ana/lab01; cd ../..; pwd", self.dir)
self.assertEqual(result.stdout, f"{self.dir}\n")
def test_cd_to_missing_folder(self):
result = shell("cd labo1", self.dir)
self.assertIn("cd: labo1: No such file or directory", result.stderr)
def test_spaces_in_a_folder_name(self):
(self.dir / "mi").mkdir()
(self.dir / "carpeta").mkdir()
result = shell("cd mi carpeta", self.dir)
self.assertIn("cd: too many arguments", result.stderr)
def test_commands_distinguish_case(self):
result = shell("Ls", self.dir)
self.assertEqual(result.returncode, 127)
self.assertIn("Ls: command not found", result.stderr)
def test_executable_needs_dot_slash(self):
compile_as("hola.c", "hola", self.dir)
result = shell("hola", self.dir)
self.assertEqual(result.returncode, 127)
self.assertIn("hola: command not found", result.stderr)
self.assertEqual(shell("./hola", self.dir).stdout, "Hola, mundo\n")
def test_source_file_is_not_executable(self):
compile_as("hola.c", "hola", self.dir)
result = shell("./hola.c", self.dir)
self.assertEqual(result.returncode, 126)
self.assertIn("./hola.c: Permission denied", result.stderr)
def test_running_before_compiling(self):
result = shell("./hola", self.dir)
self.assertEqual(result.returncode, 127)
self.assertIn("./hola: No such file or directory", result.stderr)
if __name__ == "__main__":
unittest.main()"""Pruebas del laboratorio 1. Corren sobre lab/ con `make test-lab`, y sobre
solution/ dentro de `make test`."""
import os
import subprocess
import tempfile
import unittest
from pathlib import Path
# Muestra solo el mensaje de cada prueba, sin el detalle interno de Python.
__unittest = True
HERE = Path(__file__).resolve().parent
LAB_DIR = os.environ.get("LAB_DIR", "solution")
LAB = HERE / LAB_DIR
class HolaTest(unittest.TestCase):
longMessage = False
def test_folder_exists(self):
self.assertTrue((LAB / "lab01").is_dir(),
f"No existe la carpeta {LAB_DIR}/lab01. Créenla con mkdir.")
def test_hola_compiles_and_greets(self):
source = LAB / "lab01" / "hola.c"
self.assertTrue(source.is_file(), f"No existe {LAB_DIR}/lab01/hola.c.")
with tempfile.TemporaryDirectory() as tmp:
exe = Path(tmp) / "hola"
gcc = subprocess.run(
["gcc", "-Wall", "-Wextra", "-Werror", "-std=c17", str(source), "-o", str(exe)],
capture_output=True, text=True)
self.assertEqual(gcc.returncode, 0, "hola.c no compila sin advertencias:\n" + gcc.stderr)
run = subprocess.run([str(exe)], capture_output=True, text=True, timeout=5)
self.assertEqual(run.stdout, "Hola, mundo\n", "hola debe escribir exactamente: Hola, mundo")
self.assertEqual(run.returncode, 0, "main debe terminar con return 0")
class NotesTest(unittest.TestCase):
longMessage = False
def read(self, name):
path = LAB / name
self.assertTrue(path.is_file(), f"No existe {LAB_DIR}/{name}.")
text = path.read_text(encoding="utf-8")
self.assertNotIn("___", text, f"A {name} le faltan espacios por completar (___).")
return text
def test_errors_file(self):
text = self.read("compiler_errors.txt")
for fragment in ("expected", "missing terminating"):
self.assertIn(fragment, text, f"Copien en compiler_errors.txt el mensaje de gcc ({fragment}...).")
def test_change_file(self):
text = self.read("change.txt")
start, end = text.index("algoritmo dar_vuelto"), text.index("fin algoritmo")
for word in ("mientras", "←", "≥", "fin mientras", "escribir"):
self.assertIn(word, text[start:end], f"El pseudocódigo debe usar: {word}")
position = text.index("Seguimiento")
for value in ("780", "280", "80", "30", "10", "0"):
found = text.find(value, position)
self.assertNotEqual(found, -1, f"La tabla debe pasar por el valor {value}.")
position = found + len(value)
self.assertIn("Total de monedas: 5", text)
if __name__ == "__main__":
unittest.main()Laboratorio
Archivos de partida del laboratorio de la semana.
Laboratorio 1, punto 5: el vuelto de S/ 7.80, en céntimos
Pseudocódigo (monedas de 500, 200, 100, 50, 20 y 10 céntimos):
Símbolos para copiar y pegar: ← ≥
algoritmo dar_vuelto(vuelto)
___
fin algoritmo
Seguimiento con vuelto = 780
(una fila por cada moneda entregada; agreguen las filas que hagan falta)
moneda vuelto
- 780
___ ___
Total de monedas: ___Laboratorio 1, punto 4: leer los errores de gcc
Sin el ; de la línea de printf
Línea donde avisa gcc: ___
Mensaje de gcc: ___
Sin la comilla que cierra el texto de printf
Línea del primer error: ___
Mensaje del primer error: ___
¿Cuántos errores salen en total? ___Compilación
Cómo se compilan y ejecutan los ejemplos.
# Semana 1: qué es computar. Se usa dentro del entorno del curso.
#
# make compila los programas de la clase en build/
# make test corre las pruebas de la semana y comprueba la solución del laboratorio
# make test-lab prueba su laboratorio (lab/); falla hasta que lo terminen
# make clean borra lo generado
#
# Los programas de errors/ tienen errores a propósito: no se compilan con `make`.
# Las pruebas los compilan una por una y revisan lo que dice gcc.
CC = gcc
CFLAGS = -Wall -Wextra -std=c17 -g -fsanitize=address,undefined
BUILD = build
PROGRAMS = hola change_780 exit_code
.PHONY: all test test-lab clean
all: $(PROGRAMS:%=$(BUILD)/%)
$(BUILD)/%: %.c | $(BUILD)
$(CC) $(CFLAGS) $< -o $@
$(BUILD):
mkdir -p $@
test: all
python3 -m unittest test_examples test_algorithms
@if [ -d solution ]; then LAB_DIR=solution python3 -m unittest test_lab; fi
test-lab:
LAB_DIR=lab python3 -m unittest test_lab
clean:
rm -rf $(BUILD) __pycache__