Semana 9 — Recursión
Objetivos
- Escribir funciones recursivas con un caso base y un caso recursivo correctos.
- Razonar sobre una función recursiva por inducción, sin seguir cada llamada.
- Elegir entre recursión e iteración, sabiendo qué cuesta cada una.
Teoría — Recursión: caso base, razonar por inducción, recursión e iteración
Lectura: Apuntes, semana 9.
Temas:
- La idea: resolver un problema con la solución de uno más pequeño. Factorial en el modelo de entornos: cada llamada tiene su marco.
- Salto de fe: suponer que la llamada recursiva es correcta y comprobar que el problema se achica. Relación con la inducción.
- Recursión en arreglos y cadenas: suma, máximo, palíndromo, búsqueda binaria.
- Convertir entre recursión e iteración. La pila no es infinita: stack overflow y cómo se ve en ASan y
gdb. - Depurar recursión: probar primero los casos base y usar
backtrace.
Laboratorio — Recursión en arreglos y cadenas
Sesión 1:
- Dibujar los marcos de
power(2, 3)y comprobarlo conbacktraceengdb. - Escribir, con pruebas,
array_sum,array_max,is_palindrome,count_charyreverse_string, sin ciclos.
Sesión 2:
3. Escribir búsqueda binaria recursiva y la versión rápida de power. Contar las llamadas de cada versión.
4. Escribir merge_sort recursivo sobre memoria dinámica, sin fugas.
5. Provocar un stack overflow y leer el informe.
Preguntas de repaso
- ¿Qué pasa si a una función recursiva le falta el caso base? ¿Qué mensaje da ASan?
- ¿Cómo saben que
is_palindrometermina? - ¿Cuántas llamadas hace la versión rápida de
powercon exponente 1024? ¿Por quépower(2, 1024)no cabe en unlong long? - ¿Cuándo prefieren un ciclo a una recursión?
Código de la semana
Compilar y probar dentro del contenedor del curso con make y make test.
Ejemplos (7)Errores (5)Pruebas (3)Laboratorio (11)Compilación (1)
Ejemplos
Los programas que usan los apuntes y la clase.
// Recursión sobre arreglos y cadenas, sin ciclos y sin copiar nada.
#include <stdio.h>
// Cuántos negativos hay en los n elementos que empiezan en a.
static size_t count_negative(const int *a, size_t n) {
if (n == 0) {
return 0;
}
size_t rest = count_negative(a + 1, n - 1);
if (a[0] < 0) {
return rest + 1;
}
return rest;
}
// El menor de n elementos. Requiere n >= 1: un arreglo vacío no tiene mínimo.
static int array_min(const int *a, size_t n) {
if (n == 1) {
return a[0];
}
int rest = array_min(a + 1, n - 1);
if (a[0] < rest) {
return a[0];
}
return rest;
}
// ¿El arreglo se lee igual desde los dos extremos?
static int is_symmetric(const int *a, size_t n) {
if (n <= 1) {
return 1;
}
if (a[0] != a[n - 1]) {
return 0;
}
return is_symmetric(a + 1, n - 2);
}
// Cuántos caracteres hay antes del '\0'.
static size_t string_length(const char *s) {
if (s[0] == '\0') {
return 0;
}
return 1 + string_length(s + 1);
}
int main(void) {
int data[] = {4, -2, 7, -9, 1};
int mirror[] = {3, 8, 8, 3};
size_t data_n = sizeof data / sizeof data[0];
size_t mirror_n = sizeof mirror / sizeof mirror[0];
printf("negativos: %zu\n", count_negative(data, data_n));
printf("mínimo: %d\n", array_min(data, data_n));
printf("data simétrico: %d\n", is_symmetric(data, data_n));
printf("mirror simétrico: %d\n", is_symmetric(mirror, mirror_n));
printf("largo de \"Chosica\": %zu\n", string_length("Chosica"));
printf("largo de \"\": %zu\n", string_length(""));
return 0;
}// El mismo cálculo con recursión y con un ciclo.
#include <stdio.h>
// Algoritmo de Euclides: la llamada recursiva es lo último que se hace.
static int gcd_recursive(int a, int b) {
if (b == 0) {
return a;
}
return gcd_recursive(b, a % b);
}
static int gcd_loop(int a, int b) {
while (b != 0) {
int next_b = a % b;
a = b;
b = next_b;
}
return a;
}
// Aquí queda trabajo pendiente después de la llamada: la suma.
static long long sum_recursive(int n) {
if (n <= 0) {
return 0;
}
return n + sum_recursive(n - 1);
}
// La misma suma con un acumulador: lo pendiente se hace antes de llamar.
static long long sum_accumulator(int n, long long total) {
if (n <= 0) {
return total;
}
return sum_accumulator(n - 1, total + n);
}
static long long sum_loop(int n) {
long long total = 0;
while (n > 0) {
total = total + n;
n = n - 1;
}
return total;
}
int main(void) {
printf("mcd(252, 105): %d y %d\n", gcd_recursive(252, 105), gcd_loop(252, 105));
printf("suma hasta 100: %lld, %lld y %lld\n", sum_recursive(100),
sum_accumulator(100, 0), sum_loop(100));
return 0;
}// ¿Hasta dónde llega la recursión? Uso: deep_sum recursive N, o deep_sum loop N
#include <stdio.h>
#include <string.h>
static long long sum_recursive(int n) {
if (n <= 0) {
return 0;
}
return n + sum_recursive(n - 1);
}
static long long sum_loop(int n) {
long long total = 0;
for (int i = 1; i <= n; i++) {
total = total + i;
}
return total;
}
int main(int argc, char *argv[]) {
int n;
char extra;
if (argc != 3 || sscanf(argv[2], "%d %c", &n, &extra) != 1) {
fprintf(stderr, "uso: %s recursive|loop N\n", argv[0]);
return 1;
}
if (strcmp(argv[1], "recursive") == 0) {
printf("%lld\n", sum_recursive(n));
} else if (strcmp(argv[1], "loop") == 0) {
printf("%lld\n", sum_loop(n));
} else {
fprintf(stderr, "uso: %s recursive|loop N\n", argv[0]);
return 1;
}
return 0;
}// Lo que se hace antes de la llamada ocurre al bajar; lo de después, al volver.
#include <stdio.h>
// Escribe la última cifra primero. Requiere n >= 0.
static void print_backward(int n) {
printf("%d", n % 10);
if (n >= 10) {
print_backward(n / 10);
}
}
// Escribe las cifras en orden. Requiere n >= 0.
static void print_forward(int n) {
if (n >= 10) {
print_forward(n / 10);
}
printf("%d", n % 10);
}
int main(void) {
print_backward(2026);
printf("\n");
print_forward(2026);
printf("\n");
return 0;
}// El factorial con un rastro: cada nivel de la recursión escribe con más sangría.
#include <stdio.h>
static void indent(int depth) {
for (int i = 0; i < depth; i++) {
printf(" ");
}
}
// depth solo sirve para la sangría: no interviene en la cuenta.
static long long factorial(int n, int depth) {
indent(depth);
printf("entra factorial(%d)\n", n);
if (n <= 1) {
indent(depth);
printf("caso base: devuelve 1\n");
return 1;
}
long long rest = factorial(n - 1, depth + 1);
long long result = n * rest;
indent(depth);
printf("%d * %lld = %lld\n", n, rest, result);
return result;
}
int main(void) {
printf("resultado: %lld\n", factorial(4, 0));
return 0;
}// El factorial, sin rastro: para mirar sus marcos en gdb.
#include <stdio.h>
long long factorial(int n) {
if (n <= 1) {
return 1;
}
long long rest = factorial(n - 1);
return n * rest;
}
int main(void) {
printf("factorial(4) = %lld\n", factorial(4));
return 0;
}// Cada llamada tiene su propia n: se ve en las direcciones. Cambian de una máquina a otra.
#include <stdio.h>
static int count_down(int n) {
printf("n = %d está en %p\n", n, (void *) &n);
if (n == 0) {
return 0;
}
int levels = count_down(n - 1);
printf("al volver, n sigue siendo %d\n", n);
return levels + 1;
}
int main(void) {
printf("niveles: %d\n", count_down(3));
return 0;
}Errores
Ejemplos con errores a propósito: no compilan, fallan o dan un resultado incorrecto. Los apuntes explican cada uno.
// Error a propósito: compila, no se cae y da una respuesta equivocada.
#include <stdio.h>
static int array_min(const int *a, size_t n) {
if (n == 0) {
return 0;
}
int rest = array_min(a + 1, n - 1);
if (a[0] < rest) {
return a[0];
}
return rest;
}
int main(void) {
int one[] = {5};
int many[] = {4, 2, 7};
int negative[] = {4, -2, 7};
printf("{5}: %d\n", array_min(one, 1));
printf("{4, 2, 7}: %d\n", array_min(many, 3));
printf("{4, -2, 7}: %d\n", array_min(negative, 3));
return 0;
}// Error a propósito: no hay caso base, así que las llamadas no terminan.
#include <stdio.h>
static long long sum_down(int n) {
return n + sum_down(n - 1);
}
int main(void) {
printf("%lld\n", sum_down(5));
return 0;
}// Error a propósito: 2 elevado a 1024 no cabe en un long long.
#include <stdio.h>
static long long power_slow(long long base, int n) {
if (n == 0) {
return 1;
}
return base * power_slow(base, n - 1);
}
int main(void) {
printf("2^62 = %lld\n", power_slow(2, 62));
printf("2^1024 = %lld\n", power_slow(2, 1024));
return 0;
}// Error a propósito: da bien el resultado, pero calcula dos veces la mitad.
#include <stdio.h>
static long long power_twice(long long base, int n, long *calls) {
*calls = *calls + 1;
if (n == 0) {
return 1;
}
long long result = power_twice(base, n / 2, calls) * power_twice(base, n / 2, calls);
if (n % 2 == 1) {
result = result * base;
}
return result;
}
int main(void) {
// Con base 1 el resultado siempre cabe, y el número de llamadas no depende de la base.
int sizes[] = {10, 100, 1024};
for (int i = 0; i < 3; i++) {
long calls = 0;
long long result = power_twice(1, sizes[i], &calls);
printf("power_twice(1, %d) = %lld, llamadas: %ld\n", sizes[i], result, calls);
}
return 0;
}// Error a propósito: hay caso base, pero con 7 nunca se llega a él. gcc no avisa.
#include <stdio.h>
static int half_steps(int n) {
if (n == 0) {
return 0;
}
return 1 + half_steps(n - 2);
}
int main(void) {
printf("con 6: %d\n", half_steps(6));
printf("con 7: %d\n", half_steps(7));
return 0;
}Pruebas
Comprueban lo que afirman los apuntes; se ejecutan con make test.
"""Comprueba los programas de la clase y lo que los apuntes dicen de ellos."""
import pathlib
import re
import subprocess
import tempfile
import unittest
HERE = pathlib.Path(__file__).parent
BUILD = HERE / "build"
BASE = ["-Wall", "-Wextra", "-std=c17"]
FLAGS = [*BASE, "-g", "-fsanitize=address,undefined"]
PLAIN = ["factorial", "factorial_trace", "frame_addresses", "array_examples", "convert", "digits", "deep_sum"]
def run(command, timeout=60):
done = subprocess.run(
command, cwd=HERE, text=True, encoding="utf-8", errors="replace",
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=timeout,
)
return done.stdout, done.returncode
class Compiled:
"""Un programa compilado aparte, con sus opciones, en una carpeta temporal."""
def __init__(self, source, flags):
self.folder = tempfile.TemporaryDirectory()
self.binary = str(pathlib.Path(self.folder.name) / "program")
self.messages, self.code = run(["gcc", *flags, source, "-o", self.binary])
def __enter__(self):
return self
def __exit__(self, *_):
self.folder.cleanup()
class Programs(unittest.TestCase):
def test_programs_compile_without_warnings(self):
for name in PLAIN:
with self.subTest(program=name):
with Compiled(f"{name}.c", [*FLAGS, "-Werror"]) as program:
self.assertEqual(program.code, 0, program.messages)
def test_examples_end_without_reports(self):
for name in ["factorial", "factorial_trace", "array_examples", "convert", "digits"]:
with self.subTest(program=name):
out, code = run([str(BUILD / name)])
self.assertEqual(code, 0, out)
self.assertNotIn("runtime error", out)
def test_each_call_has_its_own_n(self):
with Compiled("frame_addresses.c", [*BASE, "-g", "-O0"]) as program:
out, _ = run([program.binary])
addresses = re.findall(r"está en (0x[0-9a-f]+)", out)
self.assertEqual(len(set(addresses)), 4)
values = [int(a, 16) for a in addresses]
self.assertEqual(values, sorted(values, reverse=True), "la pila debería crecer hacia abajo")
self.assertIn("al volver, n sigue siendo 3", out)
class StackLimit(unittest.TestCase):
def report(self, binary, *args):
# En x86_64 emulado, a veces el proceso muere sin dejar el informe: se reintenta.
out = ""
for _ in range(5):
out, code = run([binary, *args])
self.assertNotEqual(code, 0)
if "stack-overflow" in out:
return out
return out
def test_gcc_warns_without_a_base_case(self):
with Compiled("errors/no_base_case.c", BASE) as program:
self.assertIn("-Winfinite-recursion", program.messages)
def test_gcc_does_not_warn_about_an_unreached_base_case(self):
with Compiled("errors/skipped_base.c", BASE) as program:
self.assertEqual(program.messages, "")
def test_asan_names_the_overflow(self):
for source in ["errors/no_base_case.c", "errors/skipped_base.c"]:
with self.subTest(source=source):
with Compiled(source, [*FLAGS, "-Wno-infinite-recursion"]) as program:
out = self.report(program.binary)
self.assertIn("AddressSanitizer: stack-overflow", out)
def test_a_correct_base_case_does_not_save_a_deep_recursion(self):
with Compiled("deep_sum.c", [*BASE, "-g", "-O0"]) as program:
self.assertEqual(run([program.binary, "recursive", "1000"])[0].strip(), "500500")
self.assertNotEqual(run([program.binary, "recursive", "10000000"])[1], 0)
self.assertEqual(run([program.binary, "loop", "10000000"])[0].strip(), "50000005000000")
def test_o2_can_remove_the_recursion(self):
# No es una garantía del lenguaje: es lo que hace este gcc, y lo que dicen los apuntes.
with Compiled("deep_sum.c", [*BASE, "-O2"]) as program:
self.assertEqual(run([program.binary, "recursive", "10000000"])[0].strip(), "50000005000000")
def test_o2_turns_a_missing_base_case_into_a_hang(self):
with Compiled("errors/no_base_case.c", [*BASE, "-O2", "-Wno-infinite-recursion"]) as program:
done, code = run(["timeout", "2", program.binary])
self.assertEqual(code, 124, done)
class WrongButQuiet(unittest.TestCase):
def test_zero_base_case_breaks_only_some_inputs(self):
with Compiled("errors/min_base_zero.c", FLAGS) as program:
out, code = run([program.binary])
self.assertEqual(code, 0)
self.assertEqual(out.split("\n")[:3], ["{5}: 0", "{4, 2, 7}: 0", "{4, -2, 7}: -2"])
def test_power_of_1024_does_not_fit(self):
with Compiled("errors/power_too_big.c", FLAGS) as program:
out, _ = run([program.binary])
self.assertIn("signed integer overflow: 2 * 4611686018427387904", out)
self.assertIn("2^62 = 4611686018427387904", out)
def test_two_calls_cost_more_than_the_slow_version(self):
with Compiled("errors/power_twice.c", BASE) as program:
out, _ = run([program.binary])
self.assertIn("power_twice(1, 1024) = 1, llamadas: 4095", out)
if __name__ == "__main__":
unittest.main()"""Pruebas del laboratorio. LAB_DIR dice dónde está el código: lab o solution.
Cada prueba compila un main propio con su código, aparte de sus pruebas. Corren con ASan,
así que una fuga también hace fallar la prueba.
"""
import os
import pathlib
import re
import subprocess
import tempfile
import unittest
HERE = pathlib.Path(__file__).parent
LAB_DIR = os.environ.get("LAB_DIR", "solution")
SOURCE = HERE / LAB_DIR
FLAGS = ["-Wall", "-Wextra", "-std=c17", "-g", "-fsanitize=address,undefined"]
INCLUDE = f"-I{HERE / 'lab'}"
HEADERS = '#include <stdio.h>\n#include <stdlib.h>\n#include <string.h>\n'
def compile_command(args):
done = subprocess.run(["gcc", *FLAGS, INCLUDE, *map(str, args)], text=True,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
if done.returncode != 0:
raise AssertionError(f"No compila:\n{done.stdout}")
def run_binary(binary, args=()):
try:
done = subprocess.run([str(binary), *args], text=True, encoding="utf-8", errors="replace",
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=30)
except subprocess.TimeoutExpired:
raise AssertionError("El programa no terminó en 30 segundos.") from None
return done.stdout, done.returncode
def build_and_run(body, header, source):
with tempfile.TemporaryDirectory() as name:
driver = pathlib.Path(name) / "driver.c"
driver.write_text(f'{HEADERS}#include "{header}"\nint main(void) {{\n{body}\nreturn 0;\n}}\n',
encoding="utf-8")
binary = pathlib.Path(name) / "driver"
compile_command([driver, SOURCE / source, "-o", binary])
return run_binary(binary)
def without_comments_and_text(path):
text = path.read_text(encoding="utf-8")
text = re.sub(r"/\*.*?\*/", " ", text, flags=re.S)
text = re.sub(r"//[^\n]*", " ", text)
text = re.sub(r'"(\\.|[^"\\])*"', '""', text)
return re.sub(r"'(\\.|[^'\\])*'", "' '", text)
class LabCase(unittest.TestCase):
longMessage = False
def check(self, result, wanted, what):
out, code = result
if code != 0:
self.fail(f"{what}: el programa terminó con error.\n{out}")
if out.strip() != wanted:
self.fail(f"{what}\n se esperaba: {wanted}\n se obtuvo: {out.strip()}")
def assert_no_loops(self, path):
code = without_comments_and_text(path)
for word in ("for", "while", "goto"):
if re.search(rf"\b{word}\b", code):
self.fail(f"{path.name} no puede usar {word}: aquí todo se resuelve con recursión.")
@unittest.skipUnless((SOURCE / "recursion.c").exists(), f"no está {LAB_DIR}/recursion.c")
class SessionOne(LabCase):
def recursion(self, body):
return build_and_run(body, "recursion.h", "recursion.c")
def test_no_loops(self):
self.assert_no_loops(SOURCE / "recursion.c")
def test_array_sum(self):
body = """
int a[] = {4, -2, 7, 2000000000, 2000000000};
printf("%lld %lld %lld %lld\\n", array_sum(NULL, 0), array_sum(a, 1), array_sum(a, 3), array_sum(a, 5));
"""
self.check(self.recursion(body), "0 4 9 4000000009", "array_sum con 0, 1, 3 y 5 elementos")
def test_array_max(self):
body = """
int a[] = {-4, -2, -7, 3, 3, 1};
printf("%d %d %d %d\\n", array_max(a, 1), array_max(a, 3), array_max(a, 6), array_max(a + 2, 1));
"""
self.check(self.recursion(body), "-4 -2 3 -7", "array_max con negativos y con repetidos")
def test_is_palindrome(self):
cases = {"": 1, "a": 1, "aa": 1, "ab": 0, "abba": 1, "reconocer": 1, "abca": 0, "abcda": 0, "Ana": 0}
for text, wanted in cases.items():
with self.subTest(text=text):
body = f'printf("%d\\n", is_palindrome("{text}") != 0);'
self.check(self.recursion(body), str(wanted), f'is_palindrome("{text}")')
def test_count_char(self):
body = """
printf("%zu %zu %zu %zu\\n", count_char("", 'a'), count_char("banana", 'a'),
count_char("banana", 'x'), count_char("Chosica", 'c'));
"""
self.check(self.recursion(body), "0 3 0 1", "count_char distingue mayúsculas y revisa la cadena completa")
def test_reverse_string(self):
for text, wanted in {"": "", "a": "a", "ab": "ba", "abc": "cba", "Lima": "amiL", "recursion": "noisrucer"}.items():
with self.subTest(text=text):
body = f'char s[] = "{text}"; reverse_string(s); printf("[%s]\\n", s);'
self.check(self.recursion(body), f"[{wanted}]", f'reverse_string("{text}")')
def test_recursion_is_really_used_on_long_input(self):
body = """
size_t n = 5000;
char *s = malloc(n + 1);
if (s == NULL) { return 1; }
memset(s, 'x', n); s[n] = '\\0';
printf("%d %zu\\n", is_palindrome(s) != 0, count_char(s, 'x'));
s[0] = 'y'; reverse_string(s);
printf("%c%c\\n", s[0], s[n - 1]);
free(s);
"""
self.check(self.recursion(body), "1 5000\nxy", "cadena de 5000 caracteres")
def test_their_own_tests_cover_every_function(self):
path = SOURCE / "recursion_tests.c"
code = path.read_text(encoding="utf-8")
for name in ["array_max", "is_palindrome", "count_char", "reverse_string"]:
with self.subTest(function=name):
if name + "(" not in code:
self.fail(f"recursion_tests.c todavía no prueba {name}.")
def test_their_own_tests_pass(self):
with tempfile.TemporaryDirectory() as name:
binary = pathlib.Path(name) / "tests"
compile_command([SOURCE / "recursion_tests.c", SOURCE / "recursion.c", "-o", binary])
out, code = run_binary(binary)
self.assertEqual(code, 0, f"Sus pruebas fallan:\n{out}")
@unittest.skipUnless((SOURCE / "counted.c").exists(), f"no está {LAB_DIR}/counted.c")
class Counting(LabCase):
def counted(self, body):
return build_and_run(body, "counted.h", "counted.c")
def test_no_loops(self):
self.assert_no_loops(SOURCE / "counted.c")
def test_power_fast_values(self):
body = """
long c = 0;
printf("%lld %lld %lld %lld %lld %lld\\n", power_fast(5, 0, &c), power_fast(2, 1, &c), power_fast(2, 10, &c),
power_fast(3, 13, &c), power_fast(-3, 3, &c), power_fast(2, 62, &c));
"""
self.check(self.counted(body), "1 2 1024 1594323 -27 4611686018427387904", "valores de power_fast")
def test_power_fast_calls(self):
for n, wanted in {0: 1, 1: 2, 10: 5, 62: 7, 1024: 12}.items():
with self.subTest(n=n):
body = f'long c = 0; power_fast(1, {n}, &c); printf("%ld\\n", c);'
self.check(self.counted(body), str(wanted), f"llamadas de power_fast(1, {n})")
def test_binary_search_finds_each_position(self):
body = """
int a[] = {1, 3, 5, 7, 9, 11, 13, 15};
long c = 0;
for (size_t n = 1; n <= 8; n++) {
for (size_t i = 0; i < n; i++) {
if (binary_search(a, n, a[i], &c) != (long) i) { printf("mal: n=%zu i=%zu\\n", n, i); }
}
}
printf("listo\\n");
"""
self.check(self.counted(body), "listo", "binary_search con cada posición y cada tamaño")
def test_binary_search_when_absent(self):
body = """
int a[] = {1, 3, 5, 7};
long c = 0;
printf("%ld %ld %ld %ld %ld\\n", binary_search(NULL, 0, 1, &c), binary_search(a, 4, 0, &c),
binary_search(a, 4, 4, &c), binary_search(a, 4, 8, &c), binary_search(a, 1, 3, &c));
"""
self.check(self.counted(body), "-1 -1 -1 -1 -1", "binary_search de un valor que no está")
def test_binary_search_with_repeated_values(self):
body = """
int a[] = {2, 2, 2, 2, 5};
long c = 0;
long i = binary_search(a, 5, 2, &c);
printf("%d %ld\\n", i >= 0 && i <= 3, binary_search(a, 5, 5, &c));
"""
self.check(self.counted(body), "1 4", "binary_search con repetidos")
def test_binary_search_really_halves(self):
body = """
size_t n = 1000000;
int *a = malloc(n * sizeof *a);
if (a == NULL) { return 1; }
for (size_t i = 0; i < n; i++) { a[i] = 2 * (int) i; }
long found = 0, absent = 0;
long i = binary_search(a, n, 1999998, &found);
long j = binary_search(a, n, 7, &absent);
printf("%ld %ld %d %d\\n", i, j, found >= 1 && found <= 22, absent >= 1 && absent <= 22);
free(a);
"""
self.check(self.counted(body), "999999 -1 1 1", "binary_search en un millón de elementos, con 22 llamadas o menos")
@unittest.skipUnless((SOURCE / "merge_sort.c").exists(), f"no está {LAB_DIR}/merge_sort.c")
class MergeSort(LabCase):
def sorted_ok(self, data):
values = ", ".join(map(str, data))
wanted = " ".join(map(str, sorted(data)))
body = f"""
int a[] = {{{values}}};
size_t n = sizeof a / sizeof a[0];
if (!merge_sort(a, n)) {{ printf("devolvió 0\\n"); return 0; }}
for (size_t i = 0; i < n; i++) {{ printf(i ? " %d" : "%d", a[i]); }}
printf("\\n");
"""
return build_and_run(body, "merge_sort.h", "merge_sort.c"), wanted
def test_small_arrays(self):
for data in ([1], [2, 1], [1, 2], [3, 1, 2], [5, 5, 5], [9, -1, 4, -1, 0], [31, 18, 25, 9, 22, 7, 14],
[8, 7, 6, 5, 4, 3, 2, 1], [1, 2, 3, 4, 5, 6, 7, 8]):
with self.subTest(data=data):
result, wanted = self.sorted_ok(data)
self.check(result, wanted, f"merge_sort de {data}")
def test_empty_and_one_element(self):
body = """
int one[] = {7};
printf("%d %d %d\\n", merge_sort(NULL, 0) != 0, merge_sort(one, 1) != 0, one[0]);
"""
self.check(build_and_run(body, "merge_sort.h", "merge_sort.c"), "1 1 7", "merge_sort de 0 y 1 elementos")
def test_large_array(self):
body = """
size_t n = 200000;
int *a = malloc(n * sizeof *a);
if (a == NULL) { return 1; }
unsigned int x = 12345;
long long total = 0, after = 0;
for (size_t i = 0; i < n; i++) {
x = x * 1103515245u + 12345u;
a[i] = (int) (x % 100000) - 50000;
total += a[i];
}
printf("%d\\n", merge_sort(a, n) != 0);
int good = 1;
for (size_t i = 0; i < n; i++) {
after += a[i];
if (i > 0 && a[i - 1] > a[i]) { good = 0; }
}
printf("%d %d\\n", good, total == after);
free(a);
"""
self.check(build_and_run(body, "merge_sort.h", "merge_sort.c"), "1\n1 1", "merge_sort de 200000 elementos")
def memory_case(self, fail_at):
"""Falla el pedido número fail_at; merge_sort se compila aparte para interceptar malloc."""
driver = HEADERS + f"""#include "merge_sort.h"
static int requests = 0;
void *test_malloc(size_t size) {{
requests++;
return requests == {fail_at} ? NULL : malloc(size);
}}
void *test_calloc(size_t count, size_t size) {{
requests++;
return requests == {fail_at} ? NULL : calloc(count, size);
}}
int main(void) {{
int a[] = {{5, 3, 8, 1, 9, 2, 7, 4, 6, 0}};
int ok = merge_sort(a, 10);
int one[] = {{1}};
requests = 0;
int small = merge_sort(one, 1) != 0 && merge_sort(NULL, 0) != 0;
printf("%d %d %d\\n", ok != 0, requests == 0, small);
return 0;
}}
"""
with tempfile.TemporaryDirectory() as name:
folder = pathlib.Path(name)
(folder / "driver.c").write_text(driver, encoding="utf-8")
compile_command(["-c", SOURCE / "merge_sort.c", "-Dmalloc=test_malloc", "-Dcalloc=test_calloc",
"-o", folder / "merge_sort.o"])
compile_command([folder / "driver.c", folder / "merge_sort.o", "-o", folder / "driver"])
return run_binary(folder / "driver")
def test_without_memory_returns_zero_and_does_not_leak(self):
self.check(self.memory_case(1), "0 1 1", "merge_sort sin memoria debe devolver 0, sin fugas")
def test_a_later_failure_does_not_leak(self):
# Si reservan más de una vez, un pedido que falla más adelante tampoco deja fugas.
out, code = self.memory_case(2)
if code != 0:
self.fail(f"merge_sort, cuando falla el segundo pedido de memoria: el programa terminó con error.\n{out}")
@unittest.skipUnless((SOURCE / "overflow.c").exists(), f"no está {LAB_DIR}/overflow.c")
class Overflow(LabCase):
def built(self, folder):
source = SOURCE / "overflow.c"
binary = pathlib.Path(folder) / "overflow"
compile_command([source, "-o", binary])
return binary
def test_sum_by_twos_ends_with_odd_numbers(self):
with tempfile.TemporaryDirectory() as name:
binary = self.built(name)
for n, wanted in {0: "0", 10: "30", 11: "36", 1: "1", -3: "0", -4: "0"}.items():
with self.subTest(n=n):
self.check(run_binary(binary, ["twos", str(n)]), wanted, f"overflow twos {n}")
def test_sum_stays_correct(self):
with tempfile.TemporaryDirectory() as name:
binary = self.built(name)
self.check(run_binary(binary, ["sum", "1000"]), "500500", "overflow sum 1000")
if __name__ == "__main__":
unittest.main()"""Vuelve a correr los comandos que aparecen en los apuntes y en las diapositivas
y comprueba que el código citado existe de verdad en esta carpeta."""
import os
import platform
import pathlib
import re
import select
import subprocess
import unittest
try:
import pty
except ImportError:
pty = None
HERE = pathlib.Path(__file__).parent
ROOT = HERE.parent.parent
NOTES = ROOT / "notes" / "week-09.md"
SLIDES = ROOT / "slides" / "week-09.tex"
# Antes de un bloque cuya salida cambia con el sistema. No se compara.
SYSTEM_DEPENDENT = "<!-- depende del sistema -->"
# Antes de un extracto de un informe de ASan, UBSan o valgrind: sus líneas deben aparecer
# en la salida real y en ese orden, sin contar direcciones, números de proceso ni rutas.
REPORT = "<!-- extracto de un informe -->"
# Antes de una sesión de gdb. Las direcciones y los números de proceso no se comparan.
GDB_SESSION = "<!-- sesión de gdb -->"
# Antes de lo que escribe demo con la solución.
DEMO_OUTPUT = "<!-- salida de demo -->"
# Las sesiones se capturaron en esta arquitectura: en otra pueden cambiar algunas líneas.
GDB_MACHINE = "aarch64"
GDB_FLAGS = ["-Wall", "-Wextra", "-std=c17", "-g", "-O0"]
GDB_SOURCES = {"factorial": ["factorial.c"], "no_base_case": ["errors/no_base_case.c"]}
def normalize(text):
# Según el idioma del entorno, gcc usa comillas tipográficas o rectas.
return text.replace("‘", "'").replace("’", "'")
def run_shell(command):
done = subprocess.run(
["bash", "-c", command], cwd=HERE, text=True, encoding="utf-8", errors="replace",
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=60,
)
return normalize(done.stdout)
def clean_lines(text):
return [line.rstrip() for line in text.splitlines()]
def split_commands(lines):
"""Parte un bloque de terminal en pares (comando, líneas de salida)."""
pairs = []
for line in lines:
if line.startswith("$ "):
pairs.append((line[2:], []))
elif pairs:
pairs[-1][1].append(normalize(line.rstrip()))
return pairs
def note_blocks(language):
"""Devuelve (líneas, marca) de cada bloque de los apuntes."""
blocks, current, mark, previous = [], None, "", ""
for raw in NOTES.read_text(encoding="utf-8").splitlines():
line = re.sub(r"^> ?", "", raw)
if current is None:
if line.strip() == "```" + language:
current, mark = [], previous.strip()
elif line.strip():
previous = line
elif line.strip() == "```":
blocks.append((current, mark))
current = None
else:
current.append(line)
return blocks
def slide_blocks(environments):
text = SLIDES.read_text(encoding="utf-8")
pattern = r"\\begin\{(?:%s)\}[^\n]*\n(.*?)\\end\{(?:%s)\}" % (environments, environments)
return [match.group(1).splitlines() for match in re.finditer(pattern, text, re.S)]
def known_code_lines():
lines = set()
sources = list(HERE.glob("*.c")) + list(HERE.glob("*.h")) + list((HERE / "errors").glob("*.c"))
sources += list((HERE / "lab").glob("*.c")) + list((HERE / "lab").glob("*.h"))
for source in sources:
lines.update(line.strip() for line in source.read_text(encoding="utf-8").splitlines())
return lines
def stable_report_line(line):
line = normalize(line).rstrip()
line = line.replace(str(HERE.resolve()) + "/", "")
line = re.sub(r"0x[0-9a-fA-F]+", "ADDR", line)
line = re.sub(r"==\d+==", "==N==", line)
return line.strip()
def assert_in_order(case, wanted, got, label):
position = 0
for line in wanted:
if not line.strip() or line.strip() == "...":
continue
with case.subTest(label=label, line=line):
found = [i for i in range(position, len(got)) if line in got[i]]
case.assertTrue(found, f"no aparece en la salida real: {line}")
if found:
position = found[0] + 1
def real_gdb_session(program, commands):
"""Corre gdb en una terminal de verdad: cada comando espera al prompt anterior."""
os.environ["TERM"] = "dumb"
pid, fd = pty.fork()
if pid == 0:
os.execvp("gdb", ["gdb", "-q", program])
def read_until_prompt():
data = b""
while not data.endswith(b"(gdb) "):
ready, _, _ = select.select([fd], [], [], 120)
if not ready:
break
try:
chunk = os.read(fd, 4096)
except OSError:
break
if not chunk:
break
data += chunk
return data
output = read_until_prompt()
for command in commands:
os.write(fd, (command + "\n").encode())
output += read_until_prompt()
os.close(fd)
os.waitpid(pid, 0)
return output.decode("utf-8", errors="replace").replace("\r", "")
def stable_gdb_lines(lines):
"""Quita lo que cambia entre corridas y deja el resto, sin las direcciones."""
stable = []
for line in lines:
line = line.rstrip()
if not line or line.startswith(("Starting program", "(gdb)", "[Thread", "Using host")):
continue
line = re.sub(r"0x[0-9a-f]+", "ADDR", line)
line = re.sub(r"process \d+", "process N", line)
# Cuántos marcos caben depende de la corrida: no se comparan esos números.
line = re.sub(r"#\d{3,}", "#N", line)
line = re.sub(r"n=-\d+", "n=M", line)
# Lo que gdb muestra del marco que cayó depende de hasta dónde llegó a escribir.
if re.match(r"(#0\s+)?sum_down \(|\s+n=<error|\s+at errors/", line):
continue
stable.append(line)
return stable
class NotesAreTrue(unittest.TestCase):
@unittest.skipUnless(NOTES.exists(), "faltan los apuntes")
def test_terminal_blocks_in_notes(self):
checked = 0
for lines, mark in note_blocks("text"):
if not lines or not lines[0].startswith("$ ") or mark in (GDB_SESSION, DEMO_OUTPUT):
continue
for command, expected in split_commands(lines):
actual = clean_lines(run_shell(command))
checked += 1
if mark == REPORT:
# El detalle de un stack overflow cambia con la arquitectura.
if platform.machine() != GDB_MACHINE:
continue
assert_in_order(self, [stable_report_line(line) for line in expected],
[stable_report_line(line) for line in actual], command)
elif mark != SYSTEM_DEPENDENT:
with self.subTest(command=command):
self.assertEqual(actual, expected)
self.assertGreater(checked, 10)
@unittest.skipUnless(NOTES.exists(), "faltan los apuntes")
def test_code_blocks_in_notes_exist(self):
known = known_code_lines()
for lines, _ in note_blocks("c"):
for line in lines:
if line.strip() and line.strip() != "...":
with self.subTest(line=line):
self.assertIn(line.strip(), known)
@unittest.skipUnless(NOTES.exists(), "faltan los apuntes")
@unittest.skipUnless(pty and platform.machine() == GDB_MACHINE, "las sesiones son de aarch64")
def test_gdb_sessions_in_notes(self):
probe = subprocess.run(
["gdb", "-q", "-batch", "-ex", "run", "/bin/true"], text=True,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
)
if "exited normally" not in probe.stdout:
self.skipTest("gdb no puede correr programas aquí")
checked = 0
for lines, mark in note_blocks("text"):
if mark != GDB_SESSION:
continue
program = lines[0].split()[-1]
name = pathlib.Path(program).name.removesuffix("_gdb")
sources = GDB_SOURCES.get(name, [name + ".c"])
subprocess.run(["gcc", *GDB_FLAGS, *sources, "-o", program], cwd=HERE, check=True,
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
commands = [line[6:].strip() for line in lines if line.startswith("(gdb) ")]
cwd = os.getcwd()
os.chdir(HERE)
try:
actual = real_gdb_session(program, commands)
finally:
os.chdir(cwd)
wanted = stable_gdb_lines(lines[1:])
got = stable_gdb_lines(actual.splitlines())
position = 0
for line in wanted:
with self.subTest(program=program, line=line):
found = [i for i in range(position, len(got)) if got[i] == line]
self.assertTrue(found, f"no aparece en la sesión real: {line}")
if found:
position = found[0] + 1
checked += 1
self.assertGreater(checked, 1)
@unittest.skipUnless(NOTES.exists() and (HERE / "solution").exists(), "falta la solución")
def test_demo_output_in_notes(self):
checked = 0
for lines, mark in note_blocks("text"):
if mark == DEMO_OUTPUT:
self.assertEqual(lines[0], "$ build/lab/demo")
actual = clean_lines(run_shell("build/solution/demo"))
self.assertEqual(actual, [line.rstrip() for line in lines[1:]])
checked += 1
self.assertGreater(checked, 0)
class SlidesAreTrue(unittest.TestCase):
@unittest.skipUnless(SLIDES.exists(), "faltan las diapositivas")
def test_terminal_blocks_in_slides(self):
# Las sesiones de gdb y los informes van en otro entorno, sesiongdb, que no se compara.
for block in slide_blocks("terminal|salida|salidalarga"):
for command, expected in split_commands(block):
actual = clean_lines(run_shell(command))
position = 0
# Una diapositiva puede omitir líneas, pero no cambiarlas ni reordenarlas.
for line in expected:
line = line.strip()
if not line:
continue
with self.subTest(command=command, line=line):
found = [i for i in range(position, len(actual)) if actual[i].strip() == line]
self.assertTrue(found, f"no aparece en la salida real: {line}")
if found:
position = found[0] + 1
@unittest.skipUnless(SLIDES.exists(), "faltan las diapositivas")
def test_code_blocks_in_slides_exist(self):
known = known_code_lines()
for block in slide_blocks("codigo|codigochico|codigomini"):
for line in block:
if line.strip() and line.strip() != "...":
with self.subTest(line=line):
self.assertIn(line.strip(), known)
if __name__ == "__main__":
unittest.main()Laboratorio
Archivos de partida del laboratorio de la semana.
// Sesión 2. Sin for, sin while y sin variables globales.
#include "counted.h"
long long power_slow(long long base, int n, long *calls) {
*calls = *calls + 1;
if (n == 0) {
return 1;
}
return base * power_slow(base, n - 1, calls);
}
static long search_from(const int *a, size_t n, size_t i, int value, long *calls) {
*calls = *calls + 1;
if (i == n) {
return -1;
}
if (a[i] == value) {
return (long) i;
}
return search_from(a, n, i + 1, value, calls);
}
long linear_search(const int *a, size_t n, int value, long *calls) {
return search_from(a, n, 0, value, calls);
}
// --- Por escribir. Lo que debe hacer cada una está en counted.h. ---
long long power_fast(long long base, int n, long *calls) {
(void) base;
(void) n;
(void) calls;
return 0;
}
// Busca en el rango [lo, hi): lo entra y hi no. Agreguen aquí su función auxiliar.
long binary_search(const int *a, size_t n, int value, long *calls) {
(void) a;
(void) n;
(void) value;
(void) calls;
return -1;
}// Sesión 2: funciones que cuentan sus propias llamadas.
//
// En todas, calls apunta a un contador que inicializa quien llama. Cada llamada a la
// función recursiva le suma 1, como primera instrucción. Así no hace falta una global.
#ifndef COUNTED_H
#define COUNTED_H
#include <stddef.h>
// base elevado a n, con una llamada por cada unidad de n. Requiere n >= 0.
long long power_slow(long long base, int n, long *calls);
// base elevado a n, partiendo n por la mitad. Requiere n >= 0.
// El caso base es n == 0, y cada llamada hace a lo sumo una llamada recursiva.
// La función recursiva es esta misma: power_fast cuenta sus propias llamadas.
long long power_fast(long long base, int n, long *calls);
// Dónde está value en los n elementos de a, o -1 si no está. Mira de izquierda a derecha.
long linear_search(const int *a, size_t n, int value, long *calls);
// Dónde está value en los n elementos de a, que están ordenados de menor a mayor,
// o -1 si no está. Si se repite, devuelve cualquiera de sus posiciones.
// Se cuentan las llamadas a la función auxiliar recursiva.
long binary_search(const int *a, size_t n, int value, long *calls);
#endif// Compara las llamadas de cada versión y prueba el ordenamiento. No se cambia.
#include <stdio.h>
#include <stdlib.h>
#include "counted.h"
#include "merge_sort.h"
#define SEARCH_SIZE 10000
static void compare_power(long long base, int n) {
long slow_calls = 0;
long fast_calls = 0;
long long slow = power_slow(base, n, &slow_calls);
long long fast = power_fast(base, n, &fast_calls);
printf("power(%lld, %d): lenta %lld con %ld llamadas, rápida %lld con %ld llamadas\n",
base, n, slow, slow_calls, fast, fast_calls);
}
static void compare_search(const int *a, size_t n, int value) {
long linear_calls = 0;
long binary_calls = 0;
long linear = linear_search(a, n, value, &linear_calls);
long binary = binary_search(a, n, value, &binary_calls);
printf("buscar %d en %zu: lineal da %ld con %ld llamadas, binaria da %ld con %ld llamadas\n",
value, n, linear, linear_calls, binary, binary_calls);
}
static void print_array(const int *a, size_t n) {
for (size_t i = 0; i < n; i++) {
printf("%d ", a[i]);
}
printf("\n");
}
int main(void) {
compare_power(2, 10);
compare_power(2, 62);
compare_power(1, 1024);
int *evens = malloc(SEARCH_SIZE * sizeof *evens);
if (evens == NULL) {
fprintf(stderr, "no hay memoria\n");
return 1;
}
for (int i = 0; i < SEARCH_SIZE; i++) {
evens[i] = 2 * i;
}
compare_search(evens, SEARCH_SIZE, 19998);
compare_search(evens, SEARCH_SIZE, 7);
free(evens);
int data[] = {31, 18, 25, 9, 22, 7, 14};
size_t data_n = sizeof data / sizeof data[0];
print_array(data, data_n);
if (!merge_sort(data, data_n)) {
fprintf(stderr, "merge_sort devolvió 0: falta memoria, o todavía no está escrita\n");
return 1;
}
print_array(data, data_n);
return 0;
}// Sesión 2. merge y sort_range son las dos partes; merge_sort reserva la memoria auxiliar.
#include <stdlib.h>
#include "merge_sort.h"
// Mezcla a[0..mid) y a[mid..n), que ya están ordenados, y deja el resultado en a.
// Usa scratch, que tiene espacio para n elementos. Aquí sí se puede usar un ciclo.
static void merge(int *a, size_t mid, size_t n, int *scratch) {
(void) a;
(void) mid;
(void) n;
(void) scratch;
}
// Ordena los n elementos de a. No reserva memoria: usa siempre el mismo scratch.
static void sort_range(int *a, size_t n, int *scratch) {
(void) a;
(void) n;
(void) scratch;
(void) merge;
}
int merge_sort(int *a, size_t n) {
(void) a;
(void) n;
(void) sort_range;
return 0;
}// Sesión 2: ordenamiento por mezcla (merge sort).
#ifndef MERGE_SORT_H
#define MERGE_SORT_H
#include <stddef.h>
// Ordena de menor a mayor los n elementos de a. Devuelve 1 si pudo, y 0 si faltó memoria.
// El arreglo es de quien llama. La memoria auxiliar es de merge_sort, que la libera siempre.
// Un arreglo de 0 o 1 elementos ya está ordenado: no pide memoria, y devuelve 1.
int merge_sort(int *a, size_t n);
#endif// Sesión 2: provocar un stack overflow a propósito. Uso: overflow twos N, o overflow sum N
#include <stdio.h>
#include <string.h>
// Suma n, n - 2, n - 4... mientras sea positivo. Con n <= 0 devuelve 0.
// Tiene un error: encuéntrenlo con el informe.
static long long sum_by_twos(int n) {
if (n == 0) {
return 0;
}
return n + sum_by_twos(n - 2);
}
// Suma n, n - 1, n - 2... Es correcta: aquí lo que se acaba es la pila.
static long long sum_to(int n) {
if (n <= 0) {
return 0;
}
return n + sum_to(n - 1);
}
int main(int argc, char *argv[]) {
int n;
char extra;
if (argc != 3 || sscanf(argv[2], "%d %c", &n, &extra) != 1) {
fprintf(stderr, "uso: %s twos|sum N\n", argv[0]);
return 1;
}
if (strcmp(argv[1], "twos") == 0) {
printf("%lld\n", sum_by_twos(n));
} else if (strcmp(argv[1], "sum") == 0) {
printf("%lld\n", sum_to(n));
} else {
fprintf(stderr, "uso: %s twos|sum N\n", argv[0]);
return 1;
}
return 0;
}// Sesión 1: dibujen los marcos de power(2, 3) en papel y compruébenlos con gdb. No se cambia.
#include <stdio.h>
static long long power(long long base, int n) {
if (n == 0) {
return 1;
}
long long rest = power(base, n - 1);
return base * rest;
}
int main(void) {
printf("power(2, 3) = %lld\n", power(2, 3));
return 0;
}# Laboratorio de la semana 9: recursión en arreglos y cadenas
Son dos sesiones, y cada una está pensada para unos 90 minutos de trabajo. Los comandos se corren
desde `code/week-09`. Las ideas están en los apuntes, en la sección "El laboratorio, paso a paso".
| Archivo | Qué es | ¿Lo cambian? |
|---|---|---|
| `power_frames.c` | `power(2, 3)` para mirar en `gdb` | No |
| `recursion.h` | Lo que promete cada función de la sesión 1 | No |
| `recursion.c` | Las cinco funciones de la sesión 1, por escribir | **Sí** |
| `recursion_tests.c` | Sus pruebas con `assert` | **Sí** |
| `counted.h` | Lo que promete cada función de la sesión 2, y cómo se cuenta | No |
| `counted.c` | `power_slow` y `linear_search` hechas; `power_fast` y `binary_search` por escribir | **Sí** |
| `merge_sort.h` | Lo que promete `merge_sort` | No |
| `merge_sort.c` | `merge`, `sort_range` y `merge_sort`, por escribir | **Sí** |
| `demo.c` | Compara las llamadas de cada versión | No |
| `overflow.c` | Una suma con un error, y una suma demasiado profunda | Solo el caso base de `sum_by_twos` |
```sh
make lab # compila recursion_tests, demo y overflow en build/lab/
build/lab/recursion_tests # corre sus pruebas
build/lab/demo # compara las llamadas, en la sesión 2
make test-lab # comprueba todo el laboratorio; falla hasta que terminen
```
Todo se compila con ASan y UBSan, salvo lo que se mira en `gdb`. `recursion.c` y `counted.c` no
pueden usar `for`, `while` ni variables globales: `make test-lab` revisa los ciclos.
## Sesión 1
### Parte 1: los marcos de `power(2, 3)` (unos 20 minutos)
Lean `power_frames.c` y, **sin ejecutar nada**, dibujen en papel la pila en el momento en que la
llamada del caso base va a devolver. Cada marco lleva `base`, `n` y `rest`, y dice en qué línea
quedó esperando. Luego compilen y compruébenlo:
```sh
gcc -Wall -Wextra -std=c17 -g -O0 lab/power_frames.c -o build/power_frames_gdb
gdb -q build/power_frames_gdb
```
Dentro de `gdb`, pongan un breakpoint condicional que se detenga solo en el caso base, corran, y pidan
`backtrace`. Con `frame` e `info args`, revisen el marco de `n = 2`. Con `finish`, vean qué devuelve
el caso base. Luego respondan por escrito:
1. ¿Cuántos marcos de `power` hay? ¿Y cuántos habría con `power(2, n)`?
2. ¿En qué línea están detenidos los marcos intermedios? ¿Por qué esa, y no otra?
3. ¿Qué valor tiene `rest` en el marco de `n = 2` antes de que vuelva la llamada? Mírenlo con
`info locals`. ¿Por qué no pueden contar con ese valor?
### Parte 2: cinco funciones sin ciclos (unos 70 minutos)
Lean `recursion.h`. Escriban en `recursion.c`, en este orden, y corran sus pruebas después de cada una:
1. `array_sum`
2. `array_max`
3. `count_char`
4. `is_palindrome`, con una función auxiliar `static`
5. `reverse_string`, con otra auxiliar
Antes de escribir cada una, respondan las tres preguntas de los apuntes: cuál es el caso base, qué baja
en cada llamada, y qué hace esta llamada con la respuesta de la siguiente.
Agreguen a `recursion_tests.c` una función de prueba por cada función, con `test_array_sum` como modelo:
primero los casos más pequeños. Para `reverse_string`, el texto debe estar en un arreglo, como
`char s[] = "Lima";`, porque una cadena literal no se puede modificar.
Y por escrito: ¿cómo saben que `is_palindrome` termina, tanto con largo par como con largo impar?
**Si no alcanza el tiempo:** lo que no terminen lo terminan antes de la próxima sesión. `reverse_string`
es la más larga, y ninguna otra depende de ella.
## Sesión 2
### Parte 3: contar llamadas (unos 30 minutos)
Lean `counted.h`, y `power_slow` y `linear_search` en `counted.c`. Fíjense en cómo se cuenta: `calls`
apunta a un contador de quien llama, y todos los marcos reciben la misma dirección.
1. Escriban `power_fast`. El caso base es `n == 0`. Guarden el resultado de la llamada en una variable:
llamar dos veces da el mismo resultado, y mucho más trabajo.
2. Escriban `binary_search`, con una auxiliar recursiva sobre el rango `[lo, hi)`.
Corran `build/lab/demo` y comparen con los apuntes. Respondan por escrito, primero contando a mano, y
luego comprobándolo con un cambio temporal en `demo.c`:
3. ¿Cuántas llamadas hace `power_fast` con `n` igual a 1000? ¿Y con 1024?
4. ¿Por qué `demo.c` usa `power(1, 1024)` y no `power(2, 1024)`? ¿Qué mostraría UBSan?
5. Si el arreglo de `demo.c` tuviera el doble de elementos, ¿cuántas llamadas más haría cada búsqueda?
### Parte 4: `merge_sort` (unos 40 minutos)
Lean `merge_sort.h` y escriban `merge_sort.c`:
1. `merge`: mezcla dos mitades ordenadas. Sigan el pseudocódigo de los apuntes. Aquí sí se puede usar un
ciclo.
2. `sort_range`: el caso base y dos llamadas. Son pocas líneas, y no reservan memoria.
3. `merge_sort`: atiende los arreglos de menos de 2 elementos, reserva `scratch`, revisa que `malloc`
haya funcionado, llama a `sort_range` y libera. Al terminarla, borren las líneas `(void)` que queden.
`build/lab/demo` termina con el arreglo antes y después de ordenar. Respondan por escrito:
6. ¿Quién es owner de `scratch`? ¿Quién podría liberarlo por error, y qué mostraría ASan?
7. ¿Qué pasaría si `merge` no copiara de vuelta el resultado a `a`? Pruébenlo.
### Parte 5: provocar un stack overflow (unos 20 minutos)
Esta parte es corta y se puede terminar antes de la próxima clase si no alcanza el tiempo.
1. Corran `build/lab/overflow twos 10`, y luego `build/lab/overflow twos 11`. Lean el informe de ASan: ¿qué
error da, en qué función y en qué línea? ¿Por qué se repite esa línea?
2. Compilen sin sanitizers y mírenlo en `gdb`:
```sh
gcc -Wall -Wextra -std=c17 -g -O0 lab/overflow.c -o build/overflow_gdb
gdb -q build/overflow_gdb
```
Dentro de `gdb`, usen `run twos 11` y `backtrace 3`. ¿Qué dicen los valores de `n` sobre el error?
3. Corrijan el caso base de `sum_by_twos`, de modo que cumpla lo que promete su comentario.
4. Busquen el límite de `sum_to`: corran `build/lab/overflow sum N` con distintos `N`, partiendo el rango
por la mitad, hasta tener el valor con una precisión de unos 1000. Anótenlo, y digan cuántos bytes
ocupa cada marco según ese número y `ulimit -s`. Ese número es de su máquina.
Al final, `make test-lab`.
## Si sobra tiempo
1. Escriban `array_max` de otra forma: partan el arreglo en dos mitades, y devuelvan el mayor de los dos
máximos. Cuenten cuántos marcos quedan apilados como máximo con un millón de elementos, frente a la
versión que quita un elemento a la vez.
2. Escriban `is_palindrome_text`, que ignore los espacios: `"anita lava la tina"` es un palíndromo.// Pruebas de la sesión 1 con assert. test_array_sum es el modelo: primero los casos base.
// Agreguen una función de prueba por cada función de recursion.h, y llámenla desde main.
#include <assert.h>
#include <stdio.h>
#include "recursion.h"
static void test_array_sum(void) {
int data[] = {4, -2, 7};
assert(array_sum(data, 0) == 0);
assert(array_sum(data, 1) == 4);
assert(array_sum(data, 3) == 9);
}
int main(void) {
test_array_sum();
printf("Todas las pruebas pasaron\n");
return 0;
}// Sesión 1. Sin for, sin while y sin variables globales. Pueden usar strlen.
// Pueden agregar funciones auxiliares static con los parámetros que necesiten.
#include "recursion.h"
long long array_sum(const int *a, size_t n) {
(void) a;
(void) n;
return 0;
}
int array_max(const int *a, size_t n) {
(void) a;
(void) n;
return 0;
}
int is_palindrome(const char *s) {
(void) s;
return 0;
}
size_t count_char(const char *s, char c) {
(void) s;
(void) c;
return 0;
}
void reverse_string(char *s) {
(void) s;
}// Sesión 1: recursión sobre arreglos y cadenas. Ninguna función usa ciclos.
#ifndef RECURSION_H
#define RECURSION_H
#include <stddef.h>
// La suma de los n elementos que empiezan en a. Un arreglo vacío suma 0.
long long array_sum(const int *a, size_t n);
// El mayor de los n elementos que empiezan en a. Requiere n >= 1.
int array_max(const int *a, size_t n);
// Devuelve 1 si s se lee igual en los dos sentidos, y 0 si no. Distingue mayúsculas.
// La cadena vacía y las de un carácter son palíndromos.
int is_palindrome(const char *s);
// Cuántas veces aparece c en s. Requiere c != '\0'.
size_t count_char(const char *s, char c);
// Invierte s en su propio lugar. No reserva memoria.
void reverse_string(char *s);
#endifCompilación
Cómo se compilan y ejecutan los ejemplos.
# Semana 9: recursión. Se usa dentro del entorno del curso.
#
# make compila los programas de la clase en build/
# make lab compila el laboratorio en build/lab/
# make test corre las pruebas de los ejemplos y de los apuntes
# make test-lab prueba su laboratorio (lab/recursion.c, lab/counted.c, lab/merge_sort.c y lab/overflow.c); falla hasta que lo terminen
# make clean borra lo generado
#
# Los programas de errors/ tienen errores a propósito. No se compilan con make:
# los compilan las pruebas, y ustedes, con los comandos de los apuntes.
# Para usar gdb se compila a mano con -g -O0 y sin sanitizers: ver los apuntes.
CC = gcc
CFLAGS = -Wall -Wextra -std=c17 -g -fsanitize=address,undefined
BUILD = build
PROGRAMS = factorial factorial_trace array_examples convert digits deep_sum
.PHONY: all lab test test-lab clean
all: $(PROGRAMS:%=$(BUILD)/%)
$(BUILD)/%: %.c | $(BUILD)
$(CC) $(CFLAGS) $< -o $@
# Cada .c se compila por separado y luego se enlaza, como en la semana 6.
$(BUILD)/lab/%.o: lab/%.c lab/recursion.h lab/counted.h lab/merge_sort.h | $(BUILD)/lab
$(CC) $(CFLAGS) -c $< -o $@
$(BUILD)/lab/recursion_tests: $(BUILD)/lab/recursion_tests.o $(BUILD)/lab/recursion.o
$(CC) $(CFLAGS) $^ -o $@
$(BUILD)/lab/demo: $(BUILD)/lab/demo.o $(BUILD)/lab/counted.o $(BUILD)/lab/merge_sort.o
$(CC) $(CFLAGS) $^ -o $@
$(BUILD)/lab/overflow: lab/overflow.c | $(BUILD)/lab
$(CC) $(CFLAGS) $< -o $@
$(BUILD)/solution/%.o: solution/%.c lab/recursion.h lab/counted.h lab/merge_sort.h | $(BUILD)/solution
$(CC) $(CFLAGS) -Ilab -c $< -o $@
$(BUILD)/solution/demo.o: lab/demo.c lab/counted.h lab/merge_sort.h | $(BUILD)/solution
$(CC) $(CFLAGS) -Ilab -c $< -o $@
$(BUILD)/solution/recursion_tests: $(BUILD)/solution/recursion_tests.o $(BUILD)/solution/recursion.o
$(CC) $(CFLAGS) $^ -o $@
$(BUILD)/solution/demo: $(BUILD)/solution/demo.o $(BUILD)/solution/counted.o $(BUILD)/solution/merge_sort.o
$(CC) $(CFLAGS) $^ -o $@
$(BUILD) $(BUILD)/lab $(BUILD)/solution:
mkdir -p $@
lab: $(BUILD)/lab/recursion_tests $(BUILD)/lab/demo $(BUILD)/lab/overflow
# Si existe solution/, también se comprueba la solución del laboratorio.
test: all
@if [ -d solution ]; then $(MAKE) --no-print-directory \
$(BUILD)/solution/recursion_tests $(BUILD)/solution/demo; fi
python3 -m unittest discover -p 'test_*.py'
test-lab: all lab
LAB_DIR=lab python3 -m unittest -v test_lab
clean:
rm -rf $(BUILD) __pycache__