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, printf y return 0. Errores de compilación y de ejecución.

Laboratorio — Entorno, terminal y primer programa

  1. Abrir el entorno siguiendo labs/README.md y correr env/check.sh. Todas las líneas deben decir ok.
  2. Usar la terminal: pwd, ls, cd, mkdir y cat. Crear la carpeta lab01.
  3. Escribir hola.c, compilarlo con gcc -Wall -Wextra -std=c17 hola.c -o hola y ejecutarlo con ./hola.
  4. Borrar a propósito un ; y luego una comilla. Anotar en qué línea avisa gcc y qué dice.
  5. Escribir en pseudocódigo el algoritmo del vuelto para 7.80 soles y seguirlo a mano.

Preguntas de repaso

  1. ¿Qué le falta a la instrucción “ordena estos números” para ser un algoritmo?
  2. Escriban en pseudocódigo un algoritmo que reciba tres números y escriba el mayor. Síganlo a mano con 4, 9, 2.
  3. ¿Qué archivo ejecuta la computadora: hola.c o hola? ¿Por qué?
  4. ¿Cuál es la diferencia entre un error de compilación y uno de ejecución?
  5. Estando en /home/ana/lab01, ¿a qué carpeta llevan cd .. y cd ../..?

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.

change_780.cDescargar change_780.c
// 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;
}
exit_code.cDescargar exit_code.c
// 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;
}
hola.cDescargar hola.c
#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.

errors/divide_by_zero.cDescargar errors/divide_by_zero.c
// 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;
}
errors/missing_include.cDescargar errors/missing_include.c
int main(void) {
    printf("Hola, mundo\n");
    return 0;
}
errors/missing_quote.cDescargar errors/missing_quote.c
#include <stdio.h>

int main(void) {
    printf("Hola, mundo\n);
    return 0;
}
errors/missing_semicolon.cDescargar errors/missing_semicolon.c
#include <stdio.h>

int main(void) {
    printf("Hola, mundo\n")
    return 0;
}
errors/misspelled_main.cDescargar errors/misspelled_main.c
#include <stdio.h>

int Main(void) {
    printf("Hola, mundo\n");
    return 0;
}
errors/misspelled_printf.cDescargar errors/misspelled_printf.c
#include <stdio.h>

int main(void) {
    print("Hola, mundo\n");
    return 0;
}
errors/no_newline.cDescargar errors/no_newline.c
#include <stdio.h>

int main(void) {
    printf("Hola, mundo");
    return 0;
}
errors/wrong_change.cDescargar errors/wrong_change.c
// 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.

test_algorithms.pyDescargar test_algorithms.py
"""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()
test_examples.pyDescargar test_examples.py
"""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()
test_lab.pyDescargar test_lab.py
"""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.

lab/change.txtDescargar lab/change.txt
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: ___
lab/compiler_errors.txtDescargar lab/compiler_errors.txt
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.

MakefileDescargar Makefile
# 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__