Semana 4 — Punteros
Objetivos
- Declarar y usar punteros, y dibujarlos en el modelo de entornos.
- Devolver varios resultados con paso por puntero.
- Relacionar arreglos y punteros, y usar
constpara declarar qué no cambia.
Teoría — Punteros: paso por puntero, arreglos y punteros, const
Lectura: Apuntes, semana 4.
Temas:
- Un puntero guarda una dirección.
&obtiene la dirección y*sigue el puntero.NULL. - Paso por puntero:
swap, y varios resultados con parámetros de salida. - Arreglos y punteros: un arreglo pasado a una función llega como puntero a su primer elemento. Aritmética de punteros y
a[i]como*(a + i). - Punteros a
structy el operador->. const char *: promesa de no modificar.FILE *y lo que en Introducción a la Computación quedó como receta.- Error común: devolver la dirección de una variable local, explicado con marcos.
Laboratorio — Transformar imágenes con punteros
Esta semana una sesión es para la Práctica 1.
Sesión 1:
- Dibujar y predecir programas cortos con punteros, y comprobarlos en
gdb. - Reescribir
invert,thresholdyflip_horizontalde la semana 3 para recibir un puntero a la imagen, y escribirimage_stats, que devuelve mínimo, máximo y promedio por parámetros de salida.
Sesión 2: Práctica 1.
Entregas
- Práctica 1: Repaso de C y modelo de entornos. Ver
practicas/README.md.
Preguntas de repaso
- Dibujen la memoria después de
int x = 5; int *p = &x; *p = 7;. ¿Cuánto valex? - ¿Por qué
int *f(void) { int x = 1; return &x; }es un error? Expliquen con marcos. - ¿Qué diferencia hay entre
const int *peint *const p? - Si
aes un arreglo deint, ¿qué significa*(a + 2)? ¿Cuántos bytes avanza?
Código de la semana
Compilar y probar dentro del contenedor del curso con make y make test.
Ejemplos (10)Errores (14)Pruebas (3)Laboratorio (12)Compilación (1)
Ejemplos
Los programas que usan los apuntes y la clase.
// Arreglos y punteros: a[i] es *(a + i), y un puntero avanza de elemento en elemento.
#include <stdio.h>
int sum_with_index(const int *a, int n) {
int total = 0;
for (int i = 0; i < n; i++) {
total = total + a[i];
}
return total;
}
int sum_with_pointer(const int *a, int n) {
int total = 0;
for (const int *p = a; p < a + n; p++) {
total = total + *p;
}
return total;
}
void show_size(const int *a) {
printf("sizeof(a) en la función = %zu\n", sizeof(a));
}
int main(void) {
int a[4] = {10, 20, 30, 40};
printf("a[2] = %d, *(a + 2) = %d\n", a[2], *(a + 2));
printf("a = %p\n", (void *) a);
printf("a + 1 = %p\n", (void *) (a + 1));
printf("a + 2 = %p\n", (void *) (a + 2));
int *p = a + 1;
printf("*p = %d, p[0] = %d, p[1] = %d\n", *p, p[0], p[1]);
int *q = &a[3];
printf("q - p = %td elementos\n", q - p);
printf("sumas: %d y %d\n", sum_with_index(a, 4), sum_with_pointer(a, 4));
printf("desde a[2]: %d\n", sum_with_pointer(a + 2, 2));
printf("sizeof(a) en main = %zu\n", sizeof(a));
show_size(a);
return 0;
}// Dónde va const: la posición dice qué es lo que no cambia.
#include <stdio.h>
int main(void) {
int a = 1;
int b = 2;
const int *to_const = &a; // no se escribe a través de él, pero puede apuntar a otro
to_const = &b;
printf("*to_const = %d\n", *to_const);
int *const fixed = &a; // se escribe a través de él, pero siempre apunta a a
*fixed = 10;
printf("a = %d\n", a);
b = 20; // const promete por el puntero, no por la variable
printf("*to_const = %d\n", *to_const);
return 0;
}// FILE *: quien llama y la función comparten el mismo archivo abierto.
#include <stdio.h>
// Se salta lo que queda de la línea actual. Devuelve 1 si había un salto de línea.
int skip_line(FILE *file) {
int c = fgetc(file);
while (c != '\n' && c != EOF) {
c = fgetc(file);
}
return c == '\n';
}
int main(void) {
FILE *file = fopen("lab/images/tiny.pgm", "r");
if (file == NULL) {
perror("lab/images/tiny.pgm");
return 1;
}
skip_line(file);
skip_line(file);
int width = 0;
int height = 0;
char line[64];
if (fgets(line, sizeof(line), file) == NULL || sscanf(line, "%d %d", &width, &height) != 2) {
printf("no se pudo leer el tamaño\n");
fclose(file);
return 1;
}
printf("tamaño: %d x %d\n", width, height);
fclose(file);
return 0;
}// El módulo de imágenes con punteros: lo que se copia y lo que no.
#include <stdio.h>
#include "lab/image.h"
void paint_copy(struct image img) {
img.pixels[0][0] = 99;
printf("En paint_copy, el primer píxel vale %d\n", img.pixels[0][0]);
}
void paint_first(struct image *img) {
img->pixels[0][0] = 99;
printf("En paint_first, el primer píxel vale %d\n", img->pixels[0][0]);
}
// Una fila de la matriz llega como puntero a su primer elemento.
void paint_row(int *row, int width) {
for (int x = 0; x < width; x++) {
row[x] = 77;
}
}
int main(void) {
struct image img;
if (!image_load(&img, "lab/images/tiny.pgm")) {
fprintf(stderr, "No se pudo leer la imagen\n");
return 1;
}
printf("sizeof(img) = %zu, sizeof(&img) = %zu\n", sizeof(img), sizeof(&img));
image_print(&img);
paint_copy(img);
printf("Después de paint_copy:\n");
image_print(&img);
paint_first(&img);
printf("Después de paint_first:\n");
image_print(&img);
paint_row(img.pixels[1], img.width);
printf("Después de paint_row:\n");
image_print(&img);
struct image other;
if (!image_load(&other, "no_existe.pgm")) {
printf("no_existe.pgm no se pudo leer\n");
}
return 0;
}// Varios resultados: el return avisa si se pudo, y los parámetros de salida traen los datos.
#include <stdio.h>
// Devuelve 1 si pudo dividir y 0 si divisor es 0. Si falla, no toca *quotient ni *remainder.
int divide(int dividend, int divisor, int *quotient, int *remainder) {
if (divisor == 0) {
return 0;
}
*quotient = dividend / divisor;
*remainder = dividend % divisor;
return 1;
}
int main(void) {
int q = -1;
int r = -1;
if (divide(17, 5, &q, &r)) {
printf("17 / 5: cociente %d, resto %d\n", q, r);
}
q = -1;
r = -1;
if (!divide(17, 0, &q, &r)) {
printf("17 / 0: no se puede, y q sigue en %d\n", q);
}
int n = 0;
printf("Escriban un número: ");
if (scanf("%d", &n) != 1) {
printf("no era un número\n");
return 1;
}
printf("leí %d\n", n);
return 0;
}// Un puntero guarda una dirección: & la obtiene y * la sigue.
#include <stdio.h>
int main(void) {
int x = 5;
int y = 20;
int *p = &x;
printf("x = %d, *p = %d\n", x, *p);
*p = 7;
printf("x = %d, *p = %d\n", x, *p);
p = &y;
*p = *p + 1;
printf("x = %d, y = %d, *p = %d\n", x, y, *p);
printf("&x = %p\n", (void *) &x);
printf("&y = %p\n", (void *) &y);
printf(" p = %p\n", (void *) p);
printf("sizeof(p) = %zu, sizeof(*p) = %zu\n", sizeof(p), sizeof(*p));
return 0;
}// Carga una imagen y la dibuja: el mismo programa de la semana 3, ahora con punteros.
#include <stdio.h>
#include "lab/image.h"
int main(void) {
struct image img;
if (!image_load(&img, "lab/images/sun.pgm")) {
fprintf(stderr, "No se pudo leer lab/images/sun.pgm\n");
return 1;
}
image_show(&img);
printf("Tamaño: %d x %d\n", img.width, img.height);
return 0;
}// Punteros a struct: el operador -> sigue el puntero y elige el campo.
#include <stdio.h>
struct product {
int stock;
double price;
};
void restock(struct product *item, int amount) {
(*item).stock = (*item).stock + amount;
}
void discount(struct product *item, double percent) {
item->price = item->price * (100 - percent) / 100;
}
double total_value(const struct product *item) {
return item->stock * item->price;
}
int main(void) {
struct product rice = {10, 4.50};
restock(&rice, 5);
discount(&rice, 20);
printf("stock %d, precio %.2f, total %.2f\n", rice.stock, rice.price, total_value(&rice));
printf("sizeof(rice) = %zu, sizeof(&rice) = %zu\n", sizeof(rice), sizeof(&rice));
return 0;
}// Dos intentos de intercambiar: uno recibe copias de los valores, el otro sus direcciones.
#include <stdio.h>
void swap_values(int a, int b) {
int old_a = a;
a = b;
b = old_a;
}
void swap(int *a, int *b) {
int old_a = *a;
*a = *b;
*b = old_a;
}
int main(void) {
int x = 1;
int y = 2;
swap_values(x, y);
printf("después de swap_values: x = %d, y = %d\n", x, y);
swap(&x, &y);
printf("después de swap: x = %d, y = %d\n", x, y);
return 0;
}// const char *: recorrer una cadena con un puntero, y guardar lo que devuelve strchr.
#include <stdio.h>
#include <string.h>
int length(const char *s) {
const char *p = s;
while (*p != '\0') {
p++;
}
return (int) (p - s);
}
int main(void) {
const char *email = "ana@example.com";
printf("%s tiene %d bytes\n", email, length(email));
const char *at = strchr(email, '@');
if (at != NULL) {
printf("dominio: %s\n", at + 1);
printf("el usuario tiene %d bytes\n", (int) (at - email));
}
char word[] = "hola";
char *first = word;
*first = 'H';
printf("%s\n", word);
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: un arreglo no se puede asignar, un puntero sí.
int main(void) {
int a[3] = {1, 2, 3};
int b[3] = {4, 5, 6};
int *p = a;
p = b;
a = b;
return *p;
}// Error a propósito: una función que promete no cambiar la imagen intenta cambiarla.
#include "../lab/image.h"
int count_pixels(const struct image *img) {
img->width = 3;
return img->width * img->height;
}
int main(void) {
struct image img;
if (!image_new(&img, 3, 2, 255, 0)) {
return 1;
}
return count_pixels(&img);
}// Error a propósito: escribe a través de un puntero a const.
int main(void) {
int a = 1;
const int *p = &a;
*p = 2;
return a;
}// Error a propósito: cambia un puntero const.
int main(void) {
int a = 1;
int b = 2;
int *const p = &a;
p = &b;
return *p;
}// Error a propósito: lo mismo que return_local.c, pero gcc no lo detecta sin optimizar.
#include <stdio.h>
int *make_value(void) {
int value = 42;
int *p = &value;
return p;
}
int other_call(void) {
int a = 1000;
int b = 2000;
return a + b;
}
int main(void) {
int *p = make_value();
printf("primero: %d\n", *p);
other_call();
printf("después: %d\n", *p);
return 0;
}// Error a propósito: guarda en un puntero normal una dirección que venía con const.
#include <stdio.h>
void reset_first(const int *a) {
int *writable = a;
*writable = 0;
}
int main(void) {
int data[2] = {1, 2};
reset_first(data);
printf("%d\n", data[0]);
return 0;
}// Error a propósito: pasa valores donde la función espera direcciones.
#include <stdio.h>
void swap(int *a, int *b) {
int old_a = *a;
*a = *b;
*b = old_a;
}
int main(void) {
int x = 1;
int y = 2;
swap(x, y);
printf("%d %d\n", x, y);
return 0;
}// Error a propósito: sigue un puntero nulo.
#include <stdio.h>
int main(void) {
int *p = NULL;
printf("antes\n");
*p = 3;
printf("después\n");
return 0;
}// Error a propósito: devuelve la dirección de una variable local.
#include <stdio.h>
int *f(void) {
int x = 1;
return &x;
}
int main(void) {
int *p = f();
printf("%d\n", *p);
return 0;
}// Error a propósito: el punto va antes que el asterisco.
struct point {
int x;
};
int get_x(const struct point *p) {
return *p.x;
}
int main(void) {
struct point a = {3};
return get_x(&a);
}// Error a propósito: quiere sumar 1 a lo apuntado, pero avanza el puntero.
#include <stdio.h>
void inc(int *p) {
*p++;
}
int main(void) {
int x = 7;
inc(&x);
printf("x = %d\n", x);
return 0;
}// Error a propósito: declara un puntero en vez de una imagen.
#include <stdio.h>
#include "../lab/image.h"
int main(void) {
struct image *img;
if (!image_load(img, "lab/images/tiny.pgm")) {
fprintf(stderr, "No se pudo leer la imagen\n");
return 1;
}
image_print(img);
return 0;
}// Error a propósito: sigue un puntero que nunca recibió una dirección.
#include <stdio.h>
int main(void) {
int *p;
*p = 3;
printf("%d\n", *p);
return 0;
}// Error a propósito: un puntero a int no puede guardar la dirección de un double.
#include <stdio.h>
int main(void) {
double d = 1.5;
int *p = &d;
printf("%d\n", *p);
return 0;
}Pruebas
Comprueban lo que afirman los apuntes; se ejecutan con make test.
"""Comprueba los programas de la clase, los errores a propósito y el módulo de imágenes."""
import pathlib
import subprocess
import tempfile
import unittest
HERE = pathlib.Path(__file__).parent
BUILD = HERE / "build"
FLAGS = ["-Wall", "-Wextra", "-std=c17", "-g", "-fsanitize=address,undefined"]
WITH_MODULE = {"image_demo", "show_sun"}
PLAIN = ["pointer_basics", "swap", "out_params", "array_pointer", "struct_pointer",
"const_pointers", "text_pointers", "file_pointer"]
def run(program, stdin=""):
done = subprocess.run(
[str(BUILD / program)], cwd=HERE, text=True, encoding="utf-8", input=stdin,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=20,
)
return done.stdout, done.returncode
def gcc_errors(name, extra=()):
"""Compila un programa de errors/ sin sanitizers y devuelve (mensajes, estado)."""
with tempfile.TemporaryDirectory() as folder:
done = subprocess.run(
["gcc", "-Wall", "-Wextra", "-std=c17", f"errors/{name}.c", *extra,
"-o", str(pathlib.Path(folder) / name)],
cwd=HERE, text=True, encoding="utf-8", stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
)
return done.stdout.replace("‘", "'").replace("’", "'"), done.returncode
def asan_run(name):
"""Compila un programa de errors/ con sanitizers, y devuelve lo que escribe al correr."""
with tempfile.TemporaryDirectory() as folder:
binary = pathlib.Path(folder) / name
subprocess.run(
["gcc", *FLAGS, f"errors/{name}.c", "-o", str(binary)],
cwd=HERE, check=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
)
done = subprocess.run(
[str(binary)], cwd=HERE, text=True, encoding="utf-8",
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=20,
)
return done.stdout, done.returncode
def run_driver(body, files=None):
"""Compila un main propio con el módulo y lo corre en una carpeta temporal."""
with tempfile.TemporaryDirectory() as name:
folder = pathlib.Path(name)
for file_name, content in (files or {}).items():
(folder / file_name).write_text(content, encoding="utf-8")
source = folder / "driver.c"
source.write_text(
'#include <stdio.h>\n#include "image.h"\nint main(void) {\n' + body + "\nreturn 0;\n}\n",
encoding="utf-8",
)
binary = folder / "driver"
subprocess.run(
["gcc", *FLAGS, "-Werror", f"-I{HERE / 'lab'}", str(source),
str(HERE / "lab" / "image.c"), "-o", str(binary)],
check=True, text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
)
done = subprocess.run(
[str(binary)], cwd=folder, text=True, encoding="utf-8",
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=20,
)
return done.stdout, done.returncode
def load_status(content):
body = 'struct image img; printf("%d\\n", image_load(&img, "in.pgm"));'
out, code = run_driver(body, {"in.pgm": content})
return out.strip(), code
class Programs(unittest.TestCase):
def test_every_program_compiles_without_warnings(self):
for name in [*PLAIN, *WITH_MODULE]:
with self.subTest(program=name):
subprocess.run(
["gcc", *FLAGS, "-Werror", "-c", f"{name}.c", "-o", "/dev/null"],
cwd=HERE, check=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
)
def test_star_writes_through_the_pointer(self):
lines = run("pointer_basics")[0].splitlines()
self.assertEqual(lines[:3], ["x = 5, *p = 5", "x = 7, *p = 7", "x = 7, y = 21, *p = 21"])
self.assertEqual(lines[4].split()[-1], lines[5].split()[-1], "p debe ser &y")
def test_swap_needs_addresses(self):
self.assertEqual(run("swap")[0].splitlines(),
["después de swap_values: x = 1, y = 2", "después de swap: x = 2, y = 1"])
def test_out_params_are_untouched_on_failure(self):
out = run("out_params", "8\n")[0]
self.assertIn("cociente 3, resto 2", out)
self.assertIn("q sigue en -1", out)
self.assertIn("leí 8", out)
def test_out_params_checks_scanf(self):
out, code = run("out_params", "abc\n")
self.assertIn("no era un número", out)
self.assertEqual(code, 1)
def test_pointer_steps_are_elements(self):
lines = run("array_pointer")[0].splitlines()
first = int(lines[1].split()[-1], 16)
second = int(lines[2].split()[-1], 16)
third = int(lines[3].split()[-1], 16)
self.assertEqual((second - first, third - first), (4, 8))
self.assertIn("q - p = 2 elementos", lines)
self.assertIn("sumas: 100 y 100", lines)
self.assertIn("desde a[2]: 70", lines)
def test_lab_pointer_programs_give_what_the_answers_say(self):
wanted = {
"pointers_basic": "a = 29, b = 20, *p = 20, *q = 29\n",
"pointers_params": "x = 4, y = 9, z = 99\n",
"pointers_array": "1 7 7 12 5 \ncount = 2, last = 1\n",
}
for name, text in wanted.items():
with self.subTest(program=name), tempfile.TemporaryDirectory() as folder:
binary = pathlib.Path(folder) / name
subprocess.run(
["gcc", *FLAGS, "-Werror", str(HERE / "lab" / f"{name}.c"), "-o", str(binary)],
check=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
)
done = subprocess.run([str(binary)], text=True, stdout=subprocess.PIPE, timeout=10)
self.assertEqual((done.stdout, done.returncode), (text, 0))
class Mistakes(unittest.TestCase):
def test_return_local_gives_a_warning_and_a_null(self):
out, code = gcc_errors("return_local")
self.assertEqual(code, 0)
self.assertIn("function returns address of local variable", out)
out, code = asan_run("return_local")
self.assertIn("null pointer", out)
self.assertNotEqual(code, 0)
def test_dangling_is_silent_for_gcc_and_caught_by_asan(self):
self.assertEqual(gcc_errors("dangling"), ("", 0))
out, code = asan_run("dangling")
self.assertIn("stack-use-after-return", out)
self.assertIn("'value'", out)
self.assertNotEqual(code, 0)
def test_null_deref_is_silent_for_gcc_and_caught_at_run_time(self):
self.assertEqual(gcc_errors("null_deref"), ("", 0))
out, code = asan_run("null_deref")
self.assertIn("store to null pointer", out)
self.assertNotEqual(code, 0)
def test_programs_that_must_not_compile(self):
wanted = {
"missing_ampersand": "makes pointer from integer",
"star_dot": "did you mean to use '->'",
"const_location": "read-only location '*p'",
"const_variable": "read-only variable 'p'",
"wrong_type": "incompatible pointer type",
}
for name, message in wanted.items():
with self.subTest(program=name):
out, code = gcc_errors(name)
self.assertNotEqual(code, 0)
self.assertIn(message, out)
out, code = gcc_errors("const_image", ["lab/image.c"])
self.assertNotEqual(code, 0)
self.assertIn("read-only object", out)
def test_programs_that_only_warn(self):
self.assertIn("value computed is not used", gcc_errors("star_increment")[0])
self.assertIn("'p' is used uninitialized", gcc_errors("uninit_pointer")[0])
self.assertIn("'img' is used uninitialized", gcc_errors("uninit_image", ["lab/image.c"])[0])
class ImageModule(unittest.TestCase):
def test_reads_comments_anywhere_between_numbers(self):
out, code = run_driver(
'struct image img; if (!image_load(&img, "in.pgm")) { return 1; } image_print(&img);',
{"in.pgm": "P2\n# uno\n2 # dos\n1\n# tres\n9\n3#cuatro\n4\n"},
)
self.assertEqual(code, 0, out)
self.assertEqual(out.split(), ["2", "x", "1,", "max", "9", "3", "4"])
def test_good_file_loads(self):
self.assertEqual(load_status("P2\n1 1\n255\n7\n"), ("1", 0))
def test_largest_size_loads(self):
pixels = " ".join(["1"] * (64 * 64))
self.assertEqual(load_status(f"P2\n64 64\n255\n{pixels}\n"), ("1", 0))
BAD_FILES = {
"formato binario": "P5\n1 1\n255\n7\n",
"vacío": "",
"ancho cero": "P2\n0 1\n255\n",
"demasiado ancho": "P2\n65 1\n255\n" + "0 " * 65,
"demasiado alto": "P2\n1 65\n255\n" + "0 " * 65,
"max_value cero": "P2\n1 1\n0\n0\n",
"max_value grande": "P2\n1 1\n256\n0\n",
"faltan píxeles": "P2\n2 2\n255\n1 2 3\n",
"píxel mayor que max": "P2\n1 1\n100\n101\n",
"píxel negativo": "P2\n1 1\n255\n-1\n",
"letra": "P2\n1 1\n255\nabc\n",
"número enorme": "P2\n99999999999999999999 1\n255\n0\n",
}
def test_bad_files_return_zero(self):
for why, content in self.BAD_FILES.items():
with self.subTest(why=why):
self.assertEqual(load_status(content), ("0", 0))
def test_missing_file_returns_zero(self):
out, code = run_driver('struct image img; printf("%d\\n", image_load(&img, "nada.pgm"));')
self.assertEqual((out.strip(), code), ("0", 0))
def test_failed_load_does_not_change_the_image(self):
body = """
struct image img;
if (!image_new(&img, 2, 1, 200, 7)) { return 1; }
printf("%d ", image_load(&img, "in.pgm"));
printf("%d %d %d %d %d", img.width, img.height, img.max_value, img.pixels[0][0], img.pixels[0][1]);
"""
# Tiene un encabezado correcto, y falla a medio camino.
out, code = run_driver(body, {"in.pgm": "P2\n3 3\n255\n1 2 3\n"})
self.assertEqual((out.split(), code), (["0", "2", "1", "200", "7", "7"], 0))
def test_save_and_load_give_the_same_image(self):
body = """
int values[64 * 3];
for (int i = 0; i < 64 * 3; i++) { values[i] = i % 256; }
struct image img;
struct image back;
printf("%d\\n", image_from_values(&img, 64, 3, 255, values));
printf("%d\\n", image_save(&img, "out.pgm"));
printf("%d\\n", image_load(&back, "out.pgm"));
printf("%d\\n", image_equal(&back, &img));
"""
out, code = run_driver(body)
self.assertEqual((out.split(), code), (["1", "1", "1", "1"], 0))
def test_saved_lines_are_at_most_70_characters(self):
body = """
struct image img;
printf("%d\\n", image_new(&img, 64, 2, 255, 255));
printf("%d\\n", image_save(&img, "out.pgm"));
FILE *file = fopen("out.pgm", "r");
if (file == NULL) { return 1; }
char line[200];
int longest = 0;
while (fgets(line, sizeof(line), file) != NULL) {
int length = 0;
while (line[length] != '\\0') { length++; }
if (length > longest) { longest = length; }
}
fclose(file);
printf("%d\\n", longest <= 71);
"""
out, code = run_driver(body)
self.assertEqual((out.split(), code), (["1", "1", "1"], 0))
def test_save_to_a_bad_path_returns_zero(self):
body = """
struct image img;
if (!image_new(&img, 1, 1, 255, 0)) { return 1; }
printf("%d\\n", image_save(&img, "no_existe/out.pgm"));
"""
out, code = run_driver(body)
self.assertEqual((out.strip(), code), ("0", 0))
def test_new_checks_its_arguments(self):
body = """
struct image img;
printf("%d", image_new(&img, 1, 1, 255, 0));
printf("%d", image_new(&img, 0, 1, 255, 0));
printf("%d", image_new(&img, 1, 65, 255, 0));
printf("%d", image_new(&img, 1, 1, 256, 0));
printf("%d", image_new(&img, 1, 1, 255, 256));
printf("%d", image_new(&img, 1, 1, 255, -1));
"""
out, code = run_driver(body)
self.assertEqual((out.strip(), code), ("100000", 0))
def test_from_values_checks_and_leaves_the_image_alone(self):
body = """
struct image img;
const int bad[] = {1, 300};
if (!image_new(&img, 2, 1, 255, 7)) { return 1; }
printf("%d ", image_from_values(&img, 2, 1, 255, bad));
printf("%d %d", img.pixels[0][0], img.pixels[0][1]);
"""
out, code = run_driver(body)
self.assertEqual((out.split(), code), (["0", "7", "7"], 0))
def test_equal_ignores_unused_cells(self):
body = """
struct image a;
if (!image_new(&a, 2, 2, 255, 7)) { return 1; }
struct image b = a;
b.pixels[5][5] = 1;
printf("%d", image_equal(&a, &b));
b.pixels[1][1] = 1;
printf("%d", image_equal(&a, &b));
"""
out, code = run_driver(body)
self.assertEqual((out.strip(), code), ("10", 0))
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 filters.c de esa carpeta y con el módulo de
lab/, sin correr las pruebas de ustedes.
"""
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"]
def compile_and_run(main_source, sources):
with tempfile.TemporaryDirectory() as name:
folder = pathlib.Path(name)
driver = folder / "driver.c"
driver.write_text(main_source, encoding="utf-8")
binary = folder / "driver"
built = subprocess.run(
["gcc", *FLAGS, f"-I{HERE / 'lab'}", str(driver), *map(str, sources), "-o", str(binary)],
text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
)
if built.returncode != 0:
raise AssertionError(f"No compila:\n{built.stdout}")
done = subprocess.run(
[str(binary)], cwd=folder, text=True, encoding="utf-8",
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=30,
)
return done.stdout, done.returncode
def with_image(body, width, height, max_value, values):
"""Corre body con la imagen img ya creada, y devuelve lo que escribe."""
numbers = ", ".join(map(str, values))
source = (
'#include <stdio.h>\n#include "filters.h"\nint main(void) {\n'
f" const int values[] = {{{numbers}}};\n"
" struct image img;\n"
f" if (!image_from_values(&img, {width}, {height}, {max_value}, values)) {{ return 2; }}\n"
f" {body}\n return 0;\n}}\n"
)
out, code = compile_and_run(source, [SOURCE / "filters.c", HERE / "lab" / "image.c"])
if code != 0:
raise AssertionError(f"El programa falló:\n{out}")
return out
def apply(call, width, height, max_value, values):
"""Devuelve (ancho, alto, max, píxeles) después de correr call sobre img."""
out = with_image(f"{call}; image_print(&img);", width, height, max_value, values)
numbers = [int(n) for n in re.findall(r"\d+", out)]
return numbers[0], numbers[1], numbers[2], numbers[3:]
def stats(width, height, max_value, values):
"""Devuelve (mínimo, máximo, promedio). Los valores de partida delatan si no se escribió."""
body = (
"int min = -1; int max = -1; double mean = -1;\n"
" image_stats(&img, &min, &max, &mean);\n"
' printf("%d %d %.4f\\n", min, max, mean);'
)
low, high, mean = with_image(body, width, height, max_value, values).split()
return int(low), int(high), float(mean)
@unittest.skipUnless((SOURCE / "filters.c").exists(), f"no está {LAB_DIR}/filters.c")
class Filters(unittest.TestCase):
longMessage = False
def check(self, what, actual, wanted):
# Un mensaje de una línea: lo que se pidió, lo que se esperaba y lo que salió.
if actual != wanted:
self.fail(f"{what}: se esperaba {wanted} y salió {actual}")
def test_invert(self):
self.check("invert(&img)", apply("invert(&img)", 3, 2, 255, [0, 10, 255, 30, 40, 100]),
(3, 2, 255, [255, 245, 0, 225, 215, 155]))
def test_invert_uses_the_max_value_of_the_image(self):
self.check("invert(&img) con max 15", apply("invert(&img)", 2, 1, 15, [0, 5]),
(2, 1, 15, [15, 10]))
def test_threshold_with_the_exact_level(self):
self.check("threshold(&img, 100)",
apply("threshold(&img, 100)", 3, 2, 255, [99, 100, 101, 0, 255, 7]),
(3, 2, 255, [0, 255, 255, 0, 255, 0]))
def test_threshold_uses_the_max_value_of_the_image(self):
self.check("threshold(&img, 8) con max 15", apply("threshold(&img, 8)", 2, 1, 15, [7, 8]),
(2, 1, 15, [0, 15]))
def test_flip_with_odd_width(self):
self.check("flip_horizontal con ancho impar",
apply("flip_horizontal(&img)", 3, 2, 255, [1, 2, 3, 4, 5, 6]),
(3, 2, 255, [3, 2, 1, 6, 5, 4]))
def test_flip_with_even_width(self):
self.check("flip_horizontal con ancho par",
apply("flip_horizontal(&img)", 4, 1, 255, [1, 2, 3, 4]),
(4, 1, 255, [4, 3, 2, 1]))
def test_filters_on_the_largest_image(self):
values = [(x * 7 + y * 11) % 256 for y in range(64) for x in range(64)]
out = apply("threshold(&img, 128); invert(&img); flip_horizontal(&img)", 64, 64, 255, values)
cut = [255 if v >= 128 else 0 for v in values]
inverted = [255 - v for v in cut]
flipped = [inverted[y * 64 + 63 - x] for y in range(64) for x in range(64)]
if out[3] != flipped:
self.fail("threshold, invert y flip_horizontal sobre una imagen de 64 x 64: "
"algún píxel no es el esperado")
def test_stats(self):
self.check("image_stats", stats(3, 2, 255, [7, 2, 3, 4, 9, 5]), (2, 9, 5.0))
def test_stats_mean_keeps_the_decimals(self):
self.check("image_stats de 0 y 1", stats(2, 1, 255, [0, 1]), (0, 1, 0.5))
def test_stats_when_the_extremes_are_at_the_borders(self):
self.check("image_stats, mínimo primero y máximo último",
stats(3, 1, 255, [1, 2, 3]), (1, 3, 2.0))
self.check("image_stats, máximo primero y mínimo último",
stats(3, 1, 255, [3, 2, 1]), (1, 3, 2.0))
def test_stats_when_every_pixel_is_the_same(self):
self.check("image_stats, todo en 0", stats(2, 2, 255, [0] * 4), (0, 0, 0.0))
self.check("image_stats, todo en 255", stats(2, 2, 255, [255] * 4), (255, 255, 255.0))
def test_stats_ignores_unused_cells(self):
# Una imagen que no es cuadrada: el 99 de relleno no es parte de ella.
body = (
"img.pixels[0][3] = 99; img.pixels[1][0] = 99;\n"
" int min = -1; int max = -1; double mean = -1;\n"
" image_stats(&img, &min, &max, &mean);\n"
' printf("%d %d %.4f\\n", min, max, mean);'
)
low, high, mean = with_image(body, 3, 1, 255, [1, 2, 3]).split()
self.check("image_stats en una imagen de 3 x 1 con basura fuera de ella",
(int(low), int(high), float(mean)), (1, 3, 2.0))
def test_stats_on_the_largest_image(self):
self.check("image_stats de 64 x 64, todo en 255",
stats(64, 64, 255, [255] * (64 * 64)), (255, 255, 255.0))
def test_stats_does_not_change_the_image(self):
out = apply("int a; int b; double c; image_stats(&img, &a, &b, &c)", 2, 1, 255, [4, 6])
self.check("la imagen después de image_stats", out, (2, 1, 255, [4, 6]))
def test_filters_compile_without_warnings(self):
built = subprocess.run(
["gcc", *FLAGS, "-Werror", f"-I{HERE / 'lab'}", "-c", str(SOURCE / "filters.c"),
"-o", "/dev/null"],
text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
)
if built.returncode != 0:
self.fail(f"filters.c da advertencias:\n{built.stdout}")
@unittest.skipUnless((SOURCE / "filter_tests.c").exists(), f"no está {LAB_DIR}/filter_tests.c")
class YourTests(unittest.TestCase):
longMessage = False
def source_without_comments(self):
text = (SOURCE / "filter_tests.c").read_text(encoding="utf-8")
return re.sub(r"//[^\n]*", "", text)
def test_each_filter_has_tests(self):
text = self.source_without_comments()
for name in ("invert", "threshold", "flip_horizontal", "image_stats"):
with self.subTest(filter=name):
if not re.search(rf"\b{name}\(", text):
self.fail(f"filter_tests.c no prueba {name}")
def test_your_tests_pass(self):
source = (SOURCE / "filter_tests.c").read_text(encoding="utf-8")
out, code = compile_and_run(source, [SOURCE / "filters.c", HERE / "lab" / "image.c"])
if code != 0:
self.fail(f"filter_tests falló:\n{out}")
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-04.md"
SLIDES = ROOT / "slides" / "week-04.tex"
# Antes de un bloque cuya salida cambia con el sistema. No se compara.
SYSTEM_DEPENDENT = "<!-- depende del sistema -->"
# Antes de una sesión de gdb. Las direcciones y los números de proceso no se comparan.
GDB_SESSION = "<!-- sesión de gdb -->"
# Igual, pero con variables sin inicializar, cuyo valor tampoco se compara.
GDB_GARBAGE = "<!-- sesión de gdb, con basura -->"
# Antes de una parte de lo que escribe filter_demo con la solución.
DEMO_OUTPUT = "<!-- salida de filter_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"]
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",
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 / "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 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], [], [], 10)
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, garbage):
"""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
if garbage and re.fullmatch(r"\w+ = -?\d+", line):
continue
line = re.sub(r"0x[0-9a-f]+", "ADDR", line)
line = re.sub(r"process \d+", "process N", line)
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.startswith("<!-- sesión de gdb"):
continue
for command, expected in split_commands(lines):
actual = clean_lines(run_shell(command))
checked += 1
if 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 not in (GDB_SESSION, GDB_GARBAGE):
continue
program = lines[0].split()[-1]
source = pathlib.Path(program).name.removesuffix("_gdb") + ".c"
subprocess.run(["gcc", *GDB_FLAGS, source, "-o", program], cwd=HERE, check=True)
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)
garbage = mark == GDB_GARBAGE
wanted = stable_gdb_lines(lines[1:], garbage)
got = stable_gdb_lines(actual.splitlines(), garbage)
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):
actual = clean_lines(run_shell("build/solution/filter_demo"))
for lines, mark in note_blocks("text"):
if mark == DEMO_OUTPUT:
text = "\n".join(line.rstrip() for line in lines)
self.assertIn(text, "\n".join(actual))
class SlidesAreTrue(unittest.TestCase):
@unittest.skipUnless(SLIDES.exists(), "faltan las diapositivas")
def test_terminal_blocks_in_slides(self):
# Las sesiones de gdb 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.
// Aplica cada filtro a una copia de la imagen, la dibuja y guarda los resultados en build/.
#include <stdio.h>
#include "filters.h"
static int show_and_save(const char *title, const struct image *img, const char *name) {
printf("%s\n", title);
image_show(img);
if (!image_save(img, name)) {
fprintf(stderr, "No se pudo escribir %s\n", name);
return 0;
}
return 1;
}
int main(void) {
struct image img;
if (!image_load(&img, "lab/images/sun.pgm")) {
fprintf(stderr, "No se pudo leer lab/images/sun.pgm\n");
return 1;
}
int min = 0;
int max = 0;
double mean = 0;
image_stats(&img, &min, &max, &mean);
printf("mínimo %d, máximo %d, promedio %.2f\n", min, max, mean);
int ok = show_and_save("Original:", &img, "build/sun_original.pgm");
// Los filtros cambian la imagen que reciben: cada uno trabaja sobre su propia copia.
struct image copy = img;
invert(©);
ok = show_and_save("invert:", ©, "build/sun_invert.pgm") && ok;
copy = img;
threshold(©, 100);
ok = show_and_save("threshold 100:", ©, "build/sun_threshold.pgm") && ok;
copy = img;
flip_horizontal(©);
ok = show_and_save("flip_horizontal:", ©, "build/sun_flip.pgm") && ok;
return ok ? 0 : 1;
}// Parte 2: pruebas de los filtros sobre imágenes pequeñas.
// Agreguen al menos una función de prueba para invert, threshold, flip_horizontal e
// image_stats, y llámenla desde main. Calculen a mano el resultado esperado.
#include <assert.h>
#include <stdio.h>
#include "filters.h"
static void test_brighten(void) {
const int values[] = {0, 10, 20,
30, 250, 255};
const int expected[] = {5, 15, 25,
35, 255, 255};
struct image img;
struct image wanted;
int ok = image_from_values(&img, 3, 2, 255, values);
ok = image_from_values(&wanted, 3, 2, 255, expected) && ok;
assert(ok);
brighten(&img, 5);
assert(image_equal(&img, &wanted));
brighten(&img, 0);
assert(image_equal(&img, &wanted));
}
int main(void) {
test_brighten();
printf("Todas las pruebas pasaron\n");
return 0;
}// Parte 2: reescriban invert, threshold y flip_horizontal, y escriban image_stats.
// brighten ya está reescrita y sirve de modelo. Lo que pide cada función está en filters.h.
#include "filters.h"
void brighten(struct image *img, int amount) {
for (int y = 0; y < img->height; y++) {
for (int x = 0; x < img->width; x++) {
int value = img->pixels[y][x] + amount;
if (value < 0) {
value = 0;
}
if (value > img->max_value) {
value = img->max_value;
}
img->pixels[y][x] = value;
}
}
}
void invert(struct image *img) {
(void) img; // borren esta línea cuando usen img
// Escriban aquí su código.
}
void threshold(struct image *img, int level) {
(void) img; // borren estas dos líneas cuando usen img y level
(void) level;
// Escriban aquí su código.
}
void flip_horizontal(struct image *img) {
(void) img; // borren esta línea cuando usen img
// Escriban aquí su código.
}
void image_stats(const struct image *img, int *min, int *max, double *mean) {
(void) img; // borren estas cuatro líneas cuando usen los parámetros
(void) min;
(void) max;
(void) mean;
// Escriban aquí su código.
}// Filtros: reciben un puntero a la imagen y la cambian ahí mismo. No devuelven nada.
#ifndef FILTERS_H
#define FILTERS_H
#include "image.h"
// Suma amount a cada píxel, sin salirse de 0 a max_value. Ya está hecho: es el modelo.
void brighten(struct image *img, int amount);
// Cada píxel p pasa a max_value - p.
void invert(struct image *img);
// Cada píxel mayor o igual que level pasa a max_value. Los demás, a 0.
void threshold(struct image *img, int level);
// Espejo izquierda-derecha: cada fila queda al revés.
void flip_horizontal(struct image *img);
// Guarda en *min y *max el menor y el mayor píxel en uso, y en *mean su promedio, con
// decimales. La imagen tiene al menos un píxel, y no cambia.
void image_stats(const struct image *img, int *min, int *max, double *mean);
#endif// Implementación del módulo de imágenes. Ver image.h.
#include "image.h"
#include <ctype.h>
#include <stdio.h>
// Ningún dato válido de un PGM de este curso pasa de aquí.
#define NUMBER_LIMIT 100000
// En cada línea del archivo: el formato pide no pasar de 70 caracteres.
#define VALUES_PER_LINE 16
static int valid_size(int width, int height, int max_value) {
return width >= 1 && width <= IMAGE_MAX_WIDTH
&& height >= 1 && height <= IMAGE_MAX_HEIGHT
&& max_value >= 1 && max_value <= IMAGE_MAX_VALUE;
}
int image_new(struct image *img, int width, int height, int max_value, int value) {
if (!valid_size(width, height, max_value) || value < 0 || value > max_value) {
return 0;
}
img->width = width;
img->height = height;
img->max_value = max_value;
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
img->pixels[y][x] = value;
}
}
return 1;
}
int image_from_values(struct image *img, int width, int height, int max_value, const int *values) {
if (!valid_size(width, height, max_value)) {
return 0;
}
// Se revisa todo antes de escribir: si algo está mal, *img no cambia.
for (int i = 0; i < width * height; i++) {
if (values[i] < 0 || values[i] > max_value) {
return 0;
}
}
img->width = width;
img->height = height;
img->max_value = max_value;
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
img->pixels[y][x] = values[y * width + x];
}
}
return 1;
}
int image_equal(const struct image *a, const struct image *b) {
if (a->width != b->width || a->height != b->height || a->max_value != b->max_value) {
return 0;
}
for (int y = 0; y < a->height; y++) {
for (int x = 0; x < a->width; x++) {
if (a->pixels[y][x] != b->pixels[y][x]) {
return 0;
}
}
}
return 1;
}
// Devuelve el siguiente número del archivo, o -1 si lo que sigue no es un número.
// Se salta los espacios y los comentarios, que van de # hasta el fin de la línea.
static int read_number(FILE *file) {
int c = fgetc(file);
while (c == '#' || isspace(c)) {
if (c == '#') {
while (c != '\n' && c != EOF) {
c = fgetc(file);
}
} else {
c = fgetc(file);
}
}
if (!isdigit(c)) {
return -1;
}
int value = 0;
while (isdigit(c)) {
value = value * 10 + (c - '0');
if (value > NUMBER_LIMIT) {
return -1;
}
c = fgetc(file);
}
// Lo que no era parte del número le toca a la siguiente lectura.
if (c != EOF) {
ungetc(c, file);
}
return value;
}
// Lee la imagen de un archivo ya abierto. Lo que venga después del último píxel se ignora.
static int read_image(struct image *img, FILE *file) {
if (fgetc(file) != 'P' || fgetc(file) != '2') {
return 0;
}
int width = read_number(file);
int height = read_number(file);
int max_value = read_number(file);
if (!image_new(img, width, height, max_value, 0)) {
return 0;
}
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
int value = read_number(file);
if (value < 0 || value > max_value) {
return 0;
}
img->pixels[y][x] = value;
}
}
return 1;
}
int image_load(struct image *img, const char *name) {
FILE *file = fopen(name, "r");
if (file == NULL) {
return 0;
}
// Se lee en una imagen local: si el archivo está mal a medias, *img no cambia.
struct image loaded;
int ok = read_image(&loaded, file);
fclose(file);
if (ok) {
*img = loaded;
}
return ok;
}
int image_save(const struct image *img, const char *name) {
if (!valid_size(img->width, img->height, img->max_value)) {
return 0;
}
FILE *file = fopen(name, "w");
if (file == NULL) {
return 0;
}
int written = fprintf(file, "P2\n%d %d\n%d\n", img->width, img->height, img->max_value);
for (int y = 0; y < img->height && written >= 0; y++) {
for (int x = 0; x < img->width && written >= 0; x++) {
int last_in_line = x == img->width - 1 || x % VALUES_PER_LINE == VALUES_PER_LINE - 1;
written = fprintf(file, "%d%c", img->pixels[y][x], last_in_line ? '\n' : ' ');
}
}
// Al cerrar se escribe lo que quedaba pendiente, y eso también puede fallar.
if (fclose(file) != 0 || written < 0) {
return 0;
}
return 1;
}
void image_print(const struct image *img) {
printf("%d x %d, max %d\n", img->width, img->height, img->max_value);
for (int y = 0; y < img->height; y++) {
for (int x = 0; x < img->width; x++) {
printf("%4d", img->pixels[y][x]);
}
printf("\n");
}
}
void image_show(const struct image *img) {
const char shades[] = " .:-=+*#%@";
int last_shade = (int) sizeof(shades) - 2;
for (int y = 0; y < img->height; y++) {
for (int x = 0; x < img->width; x++) {
putchar(shades[img->pixels[y][x] * last_shade / img->max_value]);
}
putchar('\n');
}
}// Módulo de imágenes en escala de grises, en formato PGM de texto (P2).
// Este archivo dice qué ofrece el módulo; image.c dice cómo lo hace.
//
// Desde esta semana, las imágenes se pasan por puntero. Las funciones que pueden fallar
// devuelven 1 si pudieron y 0 si no. Si fallan, no cambian *img.
#ifndef IMAGE_H
#define IMAGE_H
#define IMAGE_MAX_WIDTH 64
#define IMAGE_MAX_HEIGHT 64
#define IMAGE_MAX_VALUE 255
// Los píxeles en uso son pixels[y][x], con 0 <= y < height y 0 <= x < width.
// Cada uno vale de 0 (negro) a max_value (blanco). Las demás casillas no se usan, y
// pueden tener cualquier valor.
struct image {
int width;
int height;
int max_value;
int pixels[IMAGE_MAX_HEIGHT][IMAGE_MAX_WIDTH];
};
// Llena *img con todos sus píxeles en value. Falla si algún dato no está en su rango.
int image_new(struct image *img, int width, int height, int max_value, int value);
// Igual, pero toma los píxeles de values, fila por fila (width * height elementos).
int image_from_values(struct image *img, int width, int height, int max_value, const int *values);
// Devuelve 1 si tienen el mismo tamaño, el mismo max_value y los mismos píxeles.
int image_equal(const struct image *a, const struct image *b);
// Lee un archivo PGM de texto en *img. Falla si no se puede abrir, si no es P2, si no
// cabe o si sus datos están mal.
int image_load(struct image *img, const char *name);
// Escribe la imagen en un archivo PGM de texto. Falla si no puede escribirlo.
int image_save(const struct image *img, const char *name);
// Escribe el tamaño y los números de cada píxel, para imágenes pequeñas.
void image_print(const struct image *img);
// Dibuja la imagen en la terminal con letras: más claro, más lleno.
void image_show(const struct image *img);
#endif// Parte 1, programa 3. Dibujen los marcos en cada punto que pide lab/README.md.
#include <stdio.h>
struct counter {
int count;
int last;
};
void fill(int *start, int n, int value) {
for (int i = 0; i < n; i++) {
*(start + i) = value;
}
}
void record(struct counter *c, int value) {
c->count = c->count + 1;
c->last = value;
}
int main(void) {
int data[5] = {1, 2, 3, 4, 5};
struct counter c = {0, 0};
fill(data + 1, 2, 7);
int *p = data + 3;
*p = *(p + 1) + p[-1];
record(&c, data[3]);
record(&c, *data);
for (int i = 0; i < 5; i++) {
printf("%d ", data[i]);
}
printf("\ncount = %d, last = %d\n", c.count, c.last);
return 0;
}// Parte 1, programa 1. Dibujen las variables y las flechas en cada punto que pide lab/README.md.
#include <stdio.h>
int main(void) {
int a = 3;
int b = 8;
int *p = &a;
int *q = &b;
*p = *q + 1;
p = q;
*p = 20;
q = &a;
*q = *p + *q;
printf("a = %d, b = %d, *p = %d, *q = %d\n", a, b, *p, *q);
return 0;
}// Parte 1, programa 2. Dibujen los marcos en cada punto que pide lab/README.md.
#include <stdio.h>
void swap(int *a, int *b) {
int old_a = *a;
*a = *b;
*b = old_a;
}
void order(int *low, int *high) {
if (*low > *high) {
swap(low, high);
}
}
void retarget(int *p, int *other) {
p = other;
*p = 99;
}
int main(void) {
int x = 9;
int y = 4;
int z = 1;
order(&x, &y);
retarget(&x, &z);
printf("x = %d, y = %d, z = %d\n", x, y, z);
return 0;
}# Laboratorio de la semana 4: punteros en `gdb` y filtros por puntero
Los comandos se corren desde `code/week-04`. Las ideas están en los apuntes, en la sección
"El laboratorio, paso a paso". Esta semana hay una sola sesión de laboratorio, porque la
otra es para la Práctica 1. Está pensada para unos 90 minutos.
| Archivo | Qué es | ¿Lo cambian? |
|---|---|---|
| `pointers_basic.c`, `pointers_params.c`, `pointers_array.c` | Los tres programas de la parte 1 | No |
| `image.h`, `image.c` | El módulo de imágenes, ya reescrito con punteros | No |
| `filters.h` | Lo que debe hacer cada función | No |
| `filters.c` | Los filtros: `brighten` hecho, los demás por escribir | **Sí** |
| `filter_tests.c` | Sus pruebas con `assert` | **Sí** |
| `filter_demo.c` | Aplica los filtros a `images/sun.pgm` y dibuja el resultado | No |
| `images/` | `tiny.pgm`, de 3 x 2, y `sun.pgm`, de 32 x 16 | No |
```sh
make gdb # compila los programas de la parte 1 en build/gdb/, con -g -O0
make lab # compila filter_tests y filter_demo en build/lab/
make test-lab # comprueba sus filtros; falla hasta que terminen
```
## Parte 1: dibujar y comprobar punteros (unos 35 minutos)
Para cada programa: **primero dibujen en papel, sin ejecutar**. Cada puntero es una flecha
hacia la casilla a la que apunta. Después ábranlo en `gdb` y comparen con `print`, `whatis`,
`backtrace` y `up`. Si el dibujo y `gdb` no coinciden, averigüen por qué antes de seguir. Si a
los 35 minutos no llegaron a `pointers_array.c`, pasen a la parte 2 y déjenlo para antes de la
próxima semana.
**`pointers_basic.c`**: `gdb build/gdb/pointers_basic`.
1. Escriban lo que creen que imprime.
2. Dibujen `a`, `b`, `p` y `q` después de cada una de las líneas 10 a 14. Son cinco dibujos.
3. Con `break 10` y `next`, comprueben cada dibujo. Usen `print p`, `print &a` y `print *p`.
4. ¿Qué diferencia hay entre lo que hace la línea 11 y lo que hace la línea 12?
**`pointers_params.c`**: `gdb build/gdb/pointers_params`.
1. Escriban lo que creen que imprime.
2. Con `break swap`, dibujen los tres marcos en la parada. ¿Por qué `order` llama a
`swap(low, high)` sin `&`?
3. Comprueben con `backtrace`, y con `up` y `print low`. ¿A qué variable de `main` apuntan
`a` y `low`? Compárenlos con `print &x`.
4. Con `break 18`, dibujen el marco de `retarget` antes de `*p = 99`. Después de retornar,
¿a qué variable cambió `*p = 99`? ¿Por qué `x` no cambió, si `retarget` recibió `&x`?
**`pointers_array.c`**: `gdb build/gdb/pointers_array`.
1. Escriban lo que creen que imprime.
2. Con `break fill`, dibujen los marcos. `start` apunta a un elemento del arreglo: ¿a cuál?
Compruébenlo con `print start`, `print *start@2`, y con `up` y `print &data[1]`.
3. Con `break 27`, ¿a qué elementos se refieren `*(p + 1)` y `p[-1]`? ¿Por qué es válido un
índice negativo aquí?
4. Con `break record`, dibujen `c` en `main` y el puntero `c` en `record`. Después de la
segunda llamada, ¿qué valen `c.count` y `c.last`? ¿Qué es `*data`?
## Parte 2: los filtros por puntero (unos 55 minutos)
Tomen el cuerpo de cada filtro de `code/week-03/lab/filters.c` y pónganlo dentro de la función
que corresponde en `lab/filters.c`, sin copiar el archivo completo, porque las firmas cambiaron.
Ya no devuelven una imagen: cambian la que reciben. `brighten` es el modelo.
1. `invert`, `threshold` y `flip_horizontal`. Agreguen sus pruebas en `filter_tests.c`, con el
resultado esperado calculado a mano. `test_brighten` es el modelo.
2. `image_stats`, con sus pruebas. Devuelve tres resultados por sus parámetros de salida.
Calculen a mano una imagen de 3 x 2.
```sh
make lab && build/lab/filter_tests
```
Cuando pasen sus pruebas, corran `make test-lab`. Luego miren el resultado:
```sh
build/lab/filter_demo
```
## Para antes de la siguiente semana
Si no alcanzó el tiempo, esto es lo primero que puede quedar para después de la sesión. Lean
`image.h` e `image.c`, compárenlos con los de la semana 3 y respondan por escrito:
1. ¿Por qué `image_load` lee en una imagen local, y solo al final escribe `*img`? ¿Qué vería
quien llama si un archivo tuviera un error en el último píxel y no se hiciera así?
2. ¿Qué parámetros llevan `const` y cuáles no? ¿Qué dice eso de cada función?
3. La semana 3 usaba `width` igual a 0 para avisar de un error. ¿Qué lo reemplazó? ¿Por qué
desapareció `image_is_valid`?
4. `filter_demo.c` hace `copy = img` antes de cada filtro. ¿Qué dibujaría si no lo hiciera?
¿Cuántos bytes se copian cada vez?
5. Con `read_number(FILE *file)`, ¿qué se copia al llamarla? ¿Dónde queda guardado por dónde
va la lectura?P2
# Un sol a la izquierda y un cerro que sube hacia la derecha.
32 16
255
40 40 40 40 40 40 40 40 40 40 40 40 40 40 40 40
40 40 40 40 40 40 40 40 40 40 40 40 40 40 40 40
40 40 40 40 40 40 40 40 40 40 40 40 40 40 40 40
40 40 40 40 40 40 40 40 40 40 40 40 40 40 40 40
40 40 40 40 40 40 40 40 255 40 40 40 40 40 40 40
40 40 40 40 40 40 40 40 40 40 40 40 40 40 40 40
40 40 40 40 255 255 255 255 255 255 255 255 255 40 40 40
40 40 40 40 40 40 40 40 40 40 40 40 40 40 40 40
40 40 40 255 255 255 255 255 255 255 255 255 255 255 40 40
40 40 40 40 40 40 40 40 40 40 40 40 40 40 40 40
40 40 255 255 255 255 255 255 255 255 255 255 255 255 255 40
40 40 40 40 40 40 40 40 40 40 40 40 40 40 40 40
40 40 40 255 255 255 255 255 255 255 255 255 255 255 40 40
40 40 40 40 40 40 40 40 40 40 40 40 40 40 40 40
40 40 40 40 255 255 255 255 255 255 255 255 255 40 40 40
40 40 40 40 40 40 40 40 40 40 40 40 40 40 130 130
40 40 40 40 40 40 40 40 255 40 40 40 40 40 40 40
40 40 40 40 40 40 40 40 40 130 130 130 130 130 130 130
40 40 40 40 40 40 40 40 40 40 40 40 40 40 40 40
40 40 40 40 130 130 130 130 130 130 130 130 130 130 130 130
40 40 40 40 40 40 40 40 40 40 40 40 40 40 40 130
130 130 130 130 130 130 130 130 130 130 130 130 130 130 130 130
40 40 40 40 40 40 40 40 40 40 130 130 130 130 130 130
130 130 130 130 130 130 130 130 130 130 130 130 130 130 130 130
40 40 40 40 40 130 130 130 130 130 130 130 130 130 130 130
130 130 130 130 130 130 130 130 130 130 130 130 130 130 130 130
130 130 130 130 130 130 130 130 130 130 130 130 130 130 130 130
130 130 130 130 130 130 130 130 130 130 130 130 130 130 130 130
130 130 130 130 130 130 130 130 130 130 130 130 130 130 130 130
130 130 130 130 130 130 130 130 130 130 130 130 130 130 130 130
130 130 130 130 130 130 130 130 130 130 130 130 130 130 130 130
130 130 130 130 130 130 130 130 130 130 130 130 130 130 130 130P2
# Tres columnas y dos filas, con un comentario.
3 2
255
0 10 20
30 40 50Compilación
Cómo se compilan y ejecutan los ejemplos.
# Semana 4: punteros, y el módulo de imágenes por puntero. Se usa dentro del entorno del curso.
#
# make compila los programas de la clase en build/
# make gdb compila los programas de la parte 1 para gdb en build/gdb/ (-g -O0, sin sanitizers)
# 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/filters.c y lab/filter_tests.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.
CC = gcc
CFLAGS = -Wall -Wextra -std=c17 -g -fsanitize=address,undefined
GDBFLAGS = -Wall -Wextra -std=c17 -g -O0
BUILD = build
PROGRAMS = pointer_basics swap out_params array_pointer struct_pointer const_pointers text_pointers file_pointer
POINTERS = pointers_basic pointers_params pointers_array
MODULE = lab/image.c lab/image.h
LAB_PROGRAMS = filter_tests filter_demo
.PHONY: all gdb lab test test-lab clean
all: $(PROGRAMS:%=$(BUILD)/%) $(BUILD)/image_demo $(BUILD)/show_sun
$(BUILD)/%: %.c | $(BUILD)
$(CC) $(CFLAGS) $< -o $@
$(BUILD)/image_demo: image_demo.c $(MODULE) | $(BUILD)
$(CC) $(CFLAGS) image_demo.c lab/image.c -o $@
$(BUILD)/show_sun: show_sun.c $(MODULE) | $(BUILD)
$(CC) $(CFLAGS) show_sun.c lab/image.c -o $@
$(BUILD)/gdb/%: lab/%.c | $(BUILD)/gdb
$(CC) $(GDBFLAGS) $< -o $@
$(BUILD)/lab/filter_tests: lab/filter_tests.c lab/filters.c lab/filters.h $(MODULE) | $(BUILD)/lab
$(CC) $(CFLAGS) -Ilab lab/filter_tests.c lab/filters.c lab/image.c -o $@
$(BUILD)/lab/filter_demo: lab/filter_demo.c lab/filters.c lab/filters.h $(MODULE) | $(BUILD)/lab
$(CC) $(CFLAGS) -Ilab lab/filter_demo.c lab/filters.c lab/image.c -o $@
$(BUILD)/solution/filter_tests: solution/filter_tests.c solution/filters.c lab/filters.h $(MODULE) | $(BUILD)/solution
$(CC) $(CFLAGS) -Ilab solution/filter_tests.c solution/filters.c lab/image.c -o $@
$(BUILD)/solution/filter_demo: lab/filter_demo.c solution/filters.c lab/filters.h $(MODULE) | $(BUILD)/solution
$(CC) $(CFLAGS) -Ilab lab/filter_demo.c solution/filters.c lab/image.c -o $@
$(BUILD) $(BUILD)/gdb $(BUILD)/lab $(BUILD)/solution:
mkdir -p $@
gdb: $(POINTERS:%=$(BUILD)/gdb/%)
lab: $(LAB_PROGRAMS:%=$(BUILD)/lab/%)
# Si existe solution/, también se comprueba la solución del laboratorio.
test: all
@if [ -d solution ]; then $(MAKE) --no-print-directory \
$(LAB_PROGRAMS:%=$(BUILD)/solution/%); 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__