Semana 10 — Cadenas

Objetivos

  • Pasar arreglos a funciones y explicar por qué la función puede cambiarlos.
  • Explicar qué es una cadena en C y recorrerla.
  • Reconocer y evitar los buffer overflows.

Teoría — Arreglos y funciones · Cadenas

Lectura: Apuntes, semana 10.

Temas:

  • Pasar un arreglo a una función: se pasa su dirección, y el tamaño va aparte.
  • Mantenimiento de un vector: insertar y eliminar moviendo elementos.
  • Cadena: un arreglo de char terminado en \0. Dibujo en memoria.
  • Recorrer una cadena hasta \0. Escribir my_strlen.
  • Leer texto de forma segura con fgets y límites en scanf.
  • Error común: olvidar el espacio para \0. Buffer overflow y por qué es un problema de seguridad.

Laboratorio — Cadenas y errores de memoria

  1. Escribir sin string.h las funciones my_strlen, my_strcmp y to_upper con pruebas assert.
  2. Escribir is_palindrome, que compara bytes exactos, e is_palindrome_text, que ignora espacios y mayúsculas.
  3. Provocar un overflow con un buffer pequeño. Leer el informe de -fsanitize=address y corregirlo con fgets.

Preguntas de repaso

  1. Dibujen en memoria char s[8] = "hola";. ¿Cuánto valen my_strlen(s) y sizeof(s)?
  2. ¿Por qué una función puede cambiar un arreglo recibido, pero no un int?
  3. ¿Por qué sizeof no da el tamaño del arreglo dentro de la función?
  4. ¿Por qué es peligroso scanf("%s", s)? ¿Cómo se corrige?
  5. ¿Por qué my_strlen("año") vale 4?

Código de la semana

Compilar y probar dentro del contenedor del curso con make y make test.

Ejemplos (15)Errores (9)Pruebas (3)Laboratorio (2)Compilación (1)

Ejemplos

Los programas que usan los apuntes y la clase.

age_name.cDescargar age_name.c
// Primero scanf y después fgets: hay que descartar el resto de la línea del número.
#include <stdio.h>

#define SIZE 20

void discard_rest(void) {
    int c = getchar();
    while (c != '\n' && c != EOF) {
        c = getchar();
    }
}

int main(void) {
    int age;
    char name[SIZE];

    if (scanf("%d", &age) != 1) {
        printf("Entrada inválida\n");
        return 1;
    }
    discard_rest();
    if (fgets(name, SIZE, stdin) == NULL) {
        printf("Entrada inválida\n");
        return 1;
    }
    printf("Edad: %d\n", age);
    printf("Nombre: %s", name);
    return 0;
}
bonus.cDescargar bonus.c
// Un int se copia al llamar. Un arreglo no se copia: se pasa su dirección.
#include <stdio.h>

#define N 3

void add_one(int x) {
    x = x + 1;
}

void add_bonus(int a[], int n, int bonus) {
    for (int i = 0; i < n; i++) {
        a[i] = a[i] + bonus;
    }
}

int main(void) {
    int grade = 14;
    add_one(grade);
    printf("grade: %d\n", grade);

    int grades[N] = {14, 9, 17};
    add_bonus(grades, N, 2);
    printf("grades: %d %d %d\n", grades[0], grades[1], grades[2]);
    return 0;
}
check_strings.cDescargar check_strings.c
// Parte de las pruebas del laboratorio: test_lab.py llama con esto a sus funciones.
// No hace falta leerlo. Usa argumentos de la línea de comandos, que no vemos en el curso.
#include <stdio.h>

int my_strlen(const char s[]);
int my_strcmp(const char a[], const char b[]);
void to_upper(char s[]);
int is_palindrome(const char s[]);
int is_palindrome_text(const char s[]);

// Copia el texto a un arreglo propio, para que la función reciba memoria de la pila.
static void copy(char dest[], const char src[], int size) {
    int i = 0;
    while (src[i] != '\0' && i < size - 1) {
        dest[i] = src[i];
        i++;
    }
    dest[i] = '\0';
}

int main(int argc, char *argv[]) {
    if (argc < 3) {
        return 2;
    }
    char op = argv[1][0];
    char first[64];
    char second[64];
    copy(first, argv[2], 64);
    copy(second, argc > 3 ? argv[3] : "", 64);

    if (op == 'l') {
        printf("%d\n", my_strlen(first));
    } else if (op == 'c') {
        printf("%d\n", my_strcmp(first, second));
    } else if (op == 'u') {
        to_upper(first);
        printf("%s\n", first);
    } else if (op == 'p') {
        printf("%d\n", is_palindrome(first));
    } else if (op == 't') {
        printf("%d\n", is_palindrome_text(first));
    } else {
        return 2;
    }
    return 0;
}
const_param.cDescargar const_param.c
// const en un parámetro: la función promete que no cambia el arreglo.
#include <stdio.h>

#define N 5

// Requiere n >= 1.
int max_value(const int a[], int n) {
    int max = a[0];
    for (int i = 1; i < n; i++) {
        if (a[i] > max) {
            max = a[i];
        }
    }
    return max;
}

int main(void) {
    int grades[N] = {14, 9, 17, 11, 20};

    printf("Nota mayor: %d\n", max_value(grades, N));
    return 0;
}
copy_text.cDescargar copy_text.c
// Copia una cadena sin pasarse: quien escribe en un arreglo recibe su tamaño.
#include <stdio.h>

#define SIZE 5

// Requiere size >= 1. Copia como máximo size - 1 bytes y siempre termina en '\0'.
void copy_text(char dest[], const char src[], int size) {
    int i = 0;
    while (src[i] != '\0' && i < size - 1) {
        dest[i] = src[i];
        i++;
    }
    dest[i] = '\0';
}

int main(void) {
    char word[SIZE];

    copy_text(word, "sol", SIZE);
    printf("%s\n", word);
    copy_text(word, "luna", SIZE);
    printf("%s\n", word);
    copy_text(word, "estrella", SIZE);
    printf("%s\n", word);
    return 0;
}
count_lowercase.cDescargar count_lowercase.c
// Recorre una cadena y cuenta las minúsculas. Funciona solo para ASCII.
#include <stdio.h>

int count_lowercase(const char s[]) {
    int count = 0;
    for (int i = 0; s[i] != '\0'; i++) {
        if (s[i] >= 'a' && s[i] <= 'z') {
            count++;
        }
    }
    return count;
}

int main(void) {
    printf("Hola Mundo: %d\n", count_lowercase("Hola Mundo"));
    printf("LIMA 2026: %d\n", count_lowercase("LIMA 2026"));
    printf("ano: %d\n", count_lowercase("ano"));
    printf("año: %d\n", count_lowercase("año"));
    return 0;
}
cut_string.cDescargar cut_string.c
// %s se detiene en el primer '\0', aunque el arreglo siga.
#include <stdio.h>

int main(void) {
    char s[] = "hola";

    printf("%s\n", s);
    s[0] = 'H';
    printf("%s\n", s);
    s[2] = '\0';
    printf("%s\n", s);
    printf("sizeof(s) sigue siendo %zu\n", sizeof(s));
    return 0;
}
my_strlen.cDescargar my_strlen.c
// La longitud de una cadena: los bytes que hay antes del '\0'.
#include <assert.h>
#include <stdio.h>

int my_strlen(const char s[]) {
    int i = 0;
    while (s[i] != '\0') {
        i++;
    }
    return i;
}

int main(void) {
    assert(my_strlen("") == 0);
    assert(my_strlen("a") == 1);
    assert(my_strlen("hola") == 4);

    char t[8] = "hola";
    printf("my_strlen(t) = %d\n", my_strlen(t));
    printf("sizeof(t) = %zu\n", sizeof(t));
    printf("my_strlen(\"ano\") = %d\n", my_strlen("ano"));
    printf("my_strlen(\"año\") = %d\n", my_strlen("año"));
    return 0;
}
read_line.cDescargar read_line.c
// Muestra qué deja fgets en el buffer en cada lectura.
#include <stdio.h>

#define SIZE 8

// Escribe la cadena con \n visible, para ver si el salto de línea está.
void print_visible(const char s[]) {
    for (int i = 0; s[i] != '\0'; i++) {
        if (s[i] == '\n') {
            printf("\\n");
        } else {
            printf("%c", s[i]);
        }
    }
}

int main(void) {
    char line[SIZE];

    while (fgets(line, SIZE, stdin) != NULL) {
        printf("leído: \"");
        print_visible(line);
        printf("\"\n");
    }
    printf("fgets devolvió NULL\n");
    return 0;
}
read_name.cDescargar read_name.c
// Lee dos líneas con fgets. Quita el salto de línea y descarta lo que no cupo.
#include <stdio.h>

#define SIZE 20

// Si la cadena tiene '\n', lo quita y devuelve 1. Si no, devuelve 0.
int remove_newline(char s[]) {
    for (int i = 0; s[i] != '\0'; i++) {
        if (s[i] == '\n') {
            s[i] = '\0';
            return 1;
        }
    }
    return 0;
}

// Lee y descarta lo que queda de la línea actual.
void discard_rest(void) {
    int c = getchar();
    while (c != '\n' && c != EOF) {
        c = getchar();
    }
}

// Lee una línea en s. Devuelve 0 si la entrada ya terminó y 1 si leyó algo.
int read_text(char s[], int size) {
    if (fgets(s, size, stdin) == NULL) {
        return 0;
    }
    if (!remove_newline(s)) {
        discard_rest();
    }
    return 1;
}

int main(void) {
    char name[SIZE];
    char city[SIZE];

    if (!read_text(name, SIZE) || !read_text(city, SIZE)) {
        printf("Entrada inválida\n");
        return 1;
    }
    printf("Nombre: %s\n", name);
    printf("Ciudad: %s\n", city);
    return 0;
}
read_word.cDescargar read_word.c
// %19s lee una palabra de 19 bytes como máximo: deja lugar para el '\0'.
#include <stdio.h>

int main(void) {
    char word[20];

    if (scanf("%19s", word) != 1) {
        printf("Entrada inválida\n");
        return 1;
    }
    printf("Primera lectura: %s\n", word);

    if (scanf("%19s", word) != 1) {
        printf("No hay más palabras\n");
        return 0;
    }
    printf("Segunda lectura: %s\n", word);
    return 0;
}
same_address.cDescargar same_address.c
// La función recibe la dirección donde empieza el arreglo de main.
#include <stdio.h>

void show_address(int a[]) {
    printf("en la función, a: %p\n", (void *) a);
}

int main(void) {
    int grades[3] = {14, 9, 17};

    printf("en main, grades:  %p\n", (void *) grades);
    show_address(grades);
    return 0;
}
string_bytes.cDescargar string_bytes.c
// Muestra los bytes de dos cadenas iguales guardadas en arreglos de distinto tamaño.
#include <stdio.h>

int main(void) {
    char s[] = "hola";
    char t[8] = "hola";

    printf("sizeof(s) = %zu\n", sizeof(s));
    for (int i = 0; i < (int) sizeof(s); i++) {
        printf("s[%d] = %d\n", i, s[i]);
    }
    printf("sizeof(t) = %zu\n", sizeof(t));
    for (int i = 0; i < (int) sizeof(t); i++) {
        printf(" %d", t[i]);
    }
    printf("\n");
    printf("'\\0' vale %d y '0' vale %d\n", '\0', '0');
    return 0;
}
sum_array.cDescargar sum_array.c
// Pasa un arreglo a una función: la longitud va en otro parámetro.
#include <stdio.h>

#define N 5

int sum(int a[], int n) {
    int total = 0;
    for (int i = 0; i < n; i++) {
        total = total + a[i];
    }
    return total;
}

int main(void) {
    int grades[N] = {14, 9, 17, 11, 20};

    int total = sum(grades, N);
    printf("Suma de las 5 notas: %d\n", total);
    printf("Promedio: %.2f\n", (double) total / N);
    printf("Suma de las 3 primeras: %d\n", sum(grades, 3));
    return 0;
}
vector.cDescargar vector.c
// Mantener un vector: un arreglo con capacidad fija y una longitud que cambia.
#include <stdio.h>

#define CAPACITY 5

void print_vector(const int v[], int n) {
    printf("n = %d:", n);
    for (int i = 0; i < n; i++) {
        printf(" %d", v[i]);
    }
    printf("\n");
}

// Inserta value en la posición pos. Devuelve la longitud nueva.
// Si no se puede, no cambia nada y devuelve la misma longitud.
int insert_at(int v[], int n, int capacity, int pos, int value) {
    if (n >= capacity || pos < 0 || pos > n) {
        return n;
    }
    for (int i = n; i > pos; i--) {
        v[i] = v[i - 1];
    }
    v[pos] = value;
    return n + 1;
}

// Elimina el elemento de la posición pos. Devuelve la longitud nueva.
// Si no se puede, no cambia nada y devuelve la misma longitud.
int remove_at(int v[], int n, int pos) {
    if (pos < 0 || pos >= n) {
        return n;
    }
    for (int i = pos; i < n - 1; i++) {
        v[i] = v[i + 1];
    }
    return n - 1;
}

int main(void) {
    int v[CAPACITY] = {10, 20, 30};
    int n = 3;

    print_vector(v, n);
    n = insert_at(v, n, CAPACITY, 1, 99);
    print_vector(v, n);
    n = insert_at(v, n, CAPACITY, 4, 40);
    print_vector(v, n);
    n = insert_at(v, n, CAPACITY, 0, 5);  // ya está lleno: no cambia
    print_vector(v, n);
    n = remove_at(v, n, 0);
    print_vector(v, n);
    n = remove_at(v, n, 3);
    print_vector(v, n);
    n = remove_at(v, n, 7);               // no existe: no cambia
    print_vector(v, n);
    return 0;
}

Errores

Ejemplos con errores a propósito: no compilan, fallan o dan un resultado incorrecto. Los apuntes explican cada uno.

errors/const_write.cDescargar errors/const_write.c
// A propósito: una función con const en el parámetro intenta cambiar el arreglo.
#include <stdio.h>

void reset(const int a[], int n) {
    for (int i = 0; i < n; i++) {
        a[i] = 0;
    }
}

int main(void) {
    int grades[3] = {14, 9, 17};

    reset(grades, 3);
    printf("%d\n", grades[0]);
    return 0;
}
errors/insert_wrong.cDescargar errors/insert_wrong.c
// A propósito: abre el espacio moviendo desde el inicio y pisa los valores.
#include <stdio.h>

int main(void) {
    int v[5] = {10, 20, 30};
    int n = 3;
    int pos = 1;

    for (int i = pos; i < n; i++) {
        v[i + 1] = v[i];
    }
    v[pos] = 99;
    n = n + 1;

    for (int i = 0; i < n; i++) {
        printf(" %d", v[i]);
    }
    printf("\n");
    return 0;
}
errors/literal_write.cDescargar errors/literal_write.c
// A propósito: intenta cambiar un literal de cadena. Es comportamiento indefinido.
#include <stdio.h>

void set_first(char s[], char c) {
    s[0] = c;
}

int main(void) {
    char copy[] = "hola";

    set_first(copy, 'H');
    printf("%s\n", copy);
    set_first("hola", 'H');
    printf("no llegamos aquí, o no deberíamos\n");
    return 0;
}
errors/no_terminator.cDescargar errors/no_terminator.c
// A propósito: word tiene 4 bytes y ninguno es '\0'. No es una cadena.
#include <stdio.h>

int main(void) {
    char word[4] = "hola";

    printf("%s\n", word);
    return 0;
}
errors/overflow_neighbor.cDescargar errors/overflow_neighbor.c
// A propósito: name es pequeño y al lado puede haber otra variable importante.
#include <stdio.h>

int main(void) {
    int is_admin = 0;
    char name[8];

    if (scanf("%s", name) != 1) {
        printf("Entrada inválida\n");
        return 1;
    }
    printf("Hola, %s\n", name);
    if (is_admin != 0) {
        printf("Acceso de administrador (is_admin = %d)\n", is_admin);
    } else {
        printf("Acceso normal\n");
    }
    return 0;
}
errors/overflow_scanf.cDescargar errors/overflow_scanf.c
// A propósito: %s sin límite puede escribir fuera de name.
#include <stdio.h>

int main(void) {
    char name[8];

    if (scanf("%s", name) != 1) {
        printf("Entrada inválida\n");
        return 1;
    }
    printf("Hola, %s\n", name);
    return 0;
}
errors/scanf_then_fgets.cDescargar errors/scanf_then_fgets.c
// A propósito: scanf deja el salto de línea y fgets lo lee como una línea vacía.
#include <stdio.h>

#define SIZE 20

int main(void) {
    int age;
    char name[SIZE];

    if (scanf("%d", &age) != 1) {
        printf("Entrada inválida\n");
        return 1;
    }
    if (fgets(name, SIZE, stdin) == NULL) {
        printf("Entrada inválida\n");
        return 1;
    }
    printf("Edad: %d\n", age);
    printf("Nombre: %s", name);
    return 0;
}
errors/sizeof_param.cDescargar errors/sizeof_param.c
// A propósito: sizeof de un parámetro arreglo no da el tamaño del arreglo.
#include <stdio.h>

void print_size(int a[]) {
    printf("en la función: %zu bytes\n", sizeof(a));
}

void print_size_five(int a[5]) {
    printf("con [5] en el parámetro: %zu bytes\n", sizeof(a));
}

int main(void) {
    int grades[5] = {14, 9, 17, 11, 20};

    printf("en main: %zu bytes\n", sizeof(grades));
    print_size(grades);
    print_size_five(grades);
    return 0;
}
errors/too_long.cDescargar errors/too_long.c
// A propósito: el texto no cabe en el arreglo.
#include <stdio.h>

int main(void) {
    char word[3] = "hola";

    printf("%c\n", word[0]);
    return 0;
}

Pruebas

Comprueban lo que afirman los apuntes; se ejecutan con make test.

test_examples.pyDescargar test_examples.py
"""Comprueba lo que los apuntes afirman sobre los programas de la semana 10."""
import pathlib
import re
import subprocess
import tempfile
import unittest

HERE = pathlib.Path(__file__).parent
BUILD = HERE / "build"
FLAGS = ["-Wall", "-Wextra", "-std=c17"]
SANITIZE = ["-g", "-fsanitize=address,undefined"]


def execute(command, stdin=""):
    done = subprocess.run(
        command, input=stdin, encoding="utf-8", errors="replace",
        stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=10,
    )
    return done.stdout, done.returncode


def run(program, stdin=""):
    """Ejecuta un programa de build/; devuelve su salida (con errores) y su estado."""
    return execute([str(BUILD / program)], stdin)


def build_source(source, *extra):
    """Compila un archivo; devuelve los mensajes, el estado y el ejecutable."""
    out = pathlib.Path(tempfile.mkdtemp()) / pathlib.Path(source).stem
    done = subprocess.run(
        ["gcc", *FLAGS, *extra, source, "-o", str(out)], cwd=HERE, encoding="utf-8",
        stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
    )
    return done.stdout, done.returncode, out


class CleanCompile(unittest.TestCase):
    def test_examples_have_no_warnings(self):
        for source in sorted(HERE.glob("*.c")):
            with self.subTest(source=source.name):
                done = subprocess.run(
                    ["gcc", *FLAGS, "-Werror", "-fsyntax-only", str(source)],
                    text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
                )
                self.assertEqual(done.returncode, 0, done.stdout)

    def test_string_h_is_not_used_yet(self):
        for source in list(HERE.glob("*.c")) + list((HERE / "errors").glob("*.c")):
            with self.subTest(source=source.name):
                self.assertNotIn("string.h", source.read_text(encoding="utf-8"))


class ArraysInFunctions(unittest.TestCase):
    def test_sum_uses_the_length_it_receives(self):
        self.assertEqual(run("sum_array"), (
            "Suma de las 5 notas: 71\nPromedio: 14.20\nSuma de las 3 primeras: 40\n", 0))

    def test_function_changes_array_but_not_int(self):
        self.assertEqual(run("bonus"), ("grade: 14\ngrades: 16 11 19\n", 0))

    def test_function_sees_the_same_address(self):
        output, _ = run("same_address")
        addresses = re.findall(r"0x[0-9a-f]+", output)
        self.assertEqual(len(addresses), 2)
        self.assertEqual(addresses[0], addresses[1])

    def test_sizeof_of_parameter(self):
        messages, status, binary = build_source("errors/sizeof_param.c")
        self.assertEqual(status, 0)
        self.assertEqual(messages.count("-Wsizeof-array-argument"), 2)
        output, _ = execute([str(binary)])
        sizes = re.findall(r"(\d+) bytes", output)
        self.assertEqual(sizes[0], "20")        # 5 int de 4 bytes en este entorno
        self.assertEqual(sizes[1], sizes[2])    # el 5 del parámetro no cambia nada
        self.assertNotEqual(sizes[1], "20")

    def test_const_is_enforced(self):
        messages, status, _ = build_source("errors/const_write.c")
        self.assertNotEqual(status, 0)
        self.assertIn("read-only location", messages)

    def test_const_accepts_a_normal_array(self):
        self.assertEqual(run("const_param"), ("Nota mayor: 20\n", 0))


class Vector(unittest.TestCase):
    def test_insert_and_remove_with_their_limits(self):
        expected = ["n = 3: 10 20 30", "n = 4: 10 99 20 30", "n = 5: 10 99 20 30 40",
                    "n = 5: 10 99 20 30 40", "n = 4: 99 20 30 40", "n = 3: 99 20 30",
                    "n = 3: 99 20 30"]
        output, status = run("vector")
        self.assertEqual((output.splitlines(), status), (expected, 0))

    def test_moving_from_the_start_loses_a_value(self):
        _, _, binary = build_source("errors/insert_wrong.c")
        self.assertEqual(execute([str(binary)])[0], " 10 99 20 20\n")


class Strings(unittest.TestCase):
    def test_array_sizes_and_bytes(self):
        output, _ = run("string_bytes")
        self.assertIn("sizeof(s) = 5\n", output)
        self.assertIn("s[4] = 0\n", output)
        self.assertIn("sizeof(t) = 8\n 104 111 108 97 0 0 0 0\n", output)
        self.assertIn("'\\0' vale 0 y '0' vale 48\n", output)

    def test_terminator_cuts_the_text_but_not_the_array(self):
        self.assertEqual(run("cut_string")[0], "hola\nHola\nHo\nsizeof(s) sigue siendo 5\n")

    def test_length_counts_bytes(self):
        output, status = run("my_strlen")     # sus assert también corren aquí
        self.assertEqual(status, 0)
        self.assertIn('my_strlen("ano") = 3\n', output)
        self.assertIn('my_strlen("año") = 4\n', output)
        self.assertIn("my_strlen(t) = 4\nsizeof(t) = 8\n", output)

    def test_lowercase_ignores_utf8_with_either_char_sign(self):
        expected = "Hola Mundo: 7\nLIMA 2026: 0\nano: 3\naño: 2\n"
        self.assertEqual(run("count_lowercase")[0], expected)
        for flag in ["-fsigned-char", "-funsigned-char"]:
            with self.subTest(flag=flag):
                _, status, binary = build_source("count_lowercase.c", flag)
                self.assertEqual(status, 0)
                self.assertEqual(execute([str(binary)])[0], expected)

    def test_too_long_initializer_warns_but_one_byte_short_does_not(self):
        messages, _, _ = build_source("errors/too_long.c")
        self.assertIn("is too long", messages)
        messages, status, _ = build_source("errors/no_terminator.c")
        self.assertEqual((messages, status), ("", 0))

    def test_no_terminator_is_detected_when_running(self):
        _, _, binary = build_source("errors/no_terminator.c", *SANITIZE)
        output, status = execute([str(binary)])
        self.assertNotEqual(status, 0)
        self.assertIn("stack-buffer-overflow", output)

    def test_writing_a_literal_compiles_without_warnings(self):
        # Al correr es comportamiento indefinido: de eso no se prueba nada.
        messages, status, _ = build_source("errors/literal_write.c")
        self.assertEqual((messages, status), ("", 0))


class ReadingText(unittest.TestCase):
    def test_width_limits_scanf_and_stops_at_space(self):
        self.assertEqual(run("read_word", "Ana María\n")[0],
                         "Primera lectura: Ana\nSegunda lectura: María\n")
        self.assertEqual(run("read_word", "abcdefghijklmnopqrstuvwxyz\n")[0],
                         "Primera lectura: abcdefghijklmnopqrs\nSegunda lectura: tuvwxyz\n")
        self.assertEqual(run("read_word", ""), ("Entrada inválida\n", 1))

    def test_fgets_four_cases(self):
        cases = {
            "Ana\n": 'leído: "Ana\\n"\nfgets devolvió NULL\n',
            "Ana": 'leído: "Ana"\nfgets devolvió NULL\n',
            "Alejandra\n": 'leído: "Alejand"\nleído: "ra\\n"\nfgets devolvió NULL\n',
            "": "fgets devolvió NULL\n",
        }
        for text, expected in cases.items():
            with self.subTest(text=text):
                self.assertEqual(run("read_line", text), (expected, 0))

    def test_exact_fit_leaves_the_newline_for_later(self):
        self.assertEqual(run("read_line", "Rosaura\n")[0],
                         'leído: "Rosaura"\nleído: "\\n"\nfgets devolvió NULL\n')

    def test_read_name(self):
        self.assertEqual(run("read_name", "Ana María\nLima\n"),
                         ("Nombre: Ana María\nCiudad: Lima\n", 0))
        self.assertEqual(run("read_name", "Maria de los Angeles Quispe\nLima\n"),
                         ("Nombre: Maria de los Angele\nCiudad: Lima\n", 0))
        # 19 bytes justos: el salto de línea que queda no debe llegar a la ciudad.
        self.assertEqual(run("read_name", "abcdefghijklmnopqrs\nLima\n"),
                         ("Nombre: abcdefghijklmnopqrs\nCiudad: Lima\n", 0))
        self.assertEqual(run("read_name", "Ana\n"), ("Entrada inválida\n", 1))
        self.assertEqual(run("read_name", ""), ("Entrada inválida\n", 1))

    def test_scanf_leaves_the_newline(self):
        _, _, binary = build_source("errors/scanf_then_fgets.c")
        self.assertEqual(execute([str(binary)], "18\nAna\n")[0], "Edad: 18\nNombre: \n")
        self.assertEqual(run("age_name", "18\nAna\n")[0], "Edad: 18\nNombre: Ana\n")


class BufferOverflow(unittest.TestCase):
    def test_short_name_works_and_long_name_is_reported(self):
        messages, status, binary = build_source("errors/overflow_scanf.c", *SANITIZE)
        self.assertEqual((messages, status), ("", 0))
        self.assertEqual(execute([str(binary)], "Ana\n"), ("Hola, Ana\n", 0))
        output, status = execute([str(binary)], "Alejandra\n")
        self.assertNotEqual(status, 0)
        for fragment in ["stack-buffer-overflow", "WRITE of size 10",
                         "overflow_scanf.c:7", "'name' (line 5)"]:
            self.assertIn(fragment, output)

    def test_overflow_demos_compile_without_warnings(self):
        for source in ["errors/overflow_scanf.c", "errors/overflow_neighbor.c"]:
            with self.subTest(source=source):
                messages, status, _ = build_source(source)
                self.assertEqual((messages, status), ("", 0))

    def test_neighbor_demo_is_caught_by_the_sanitizer(self):
        # Sin sanitizer el resultado depende de la máquina: no se compara.
        _, _, binary = build_source("errors/overflow_neighbor.c", *SANITIZE)
        self.assertIn("Acceso normal", execute([str(binary)], "Ana\n")[0])
        self.assertIn("stack-buffer-overflow", execute([str(binary)], "Alejandra_Quispe\n")[0])

    def test_copy_text_stays_inside(self):
        self.assertEqual(run("copy_text"), ("sol\nluna\nestr\n", 0))


if __name__ == "__main__":
    unittest.main()
test_lab.pyDescargar test_lab.py
"""Pruebas del laboratorio. LAB_DIR dice dónde está el código: lab o solution."""
import os
import pathlib
import subprocess
import unittest

HERE = pathlib.Path(__file__).parent
LAB_DIR = os.environ.get("LAB_DIR", "solution")


def run(program, stdin=""):
    done = subprocess.run(
        [str(HERE / "build" / LAB_DIR / program)], input=stdin, encoding="utf-8", errors="replace",
        stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=10,
    )
    return done.stdout, done.returncode


def call(*args):
    """Llama a una función de my_strings.c y devuelve lo que imprime el programa de prueba."""
    done = subprocess.run(
        [str(HERE / "build" / LAB_DIR / "check_strings")] + [a.encode("utf-8") for a in args],
        stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=10,
    )
    return done.stdout.decode("utf-8", "replace").strip()


@unittest.skipUnless((HERE / LAB_DIR).is_dir(), f"no existe {LAB_DIR}/")
class LabTests(unittest.TestCase):
    def test_no_warnings(self):
        for name in ["my_strings", "greet"]:
            with self.subTest(name=name):
                done = subprocess.run(
                    ["gcc", "-Wall", "-Wextra", "-std=c17", "-Werror", "-fsyntax-only",
                     str(HERE / LAB_DIR / f"{name}.c")],
                    text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
                )
                self.assertEqual(done.returncode, 0, done.stdout)

    def test_does_not_use_string_h(self):
        text = (HERE / LAB_DIR / "my_strings.c").read_text(encoding="utf-8")
        self.assertNotRegex(text, r"#\s*include\s*<string\.h>")

    def test_your_own_asserts_pass(self):
        output, code = run("my_strings")
        self.assertEqual((output, code), ("Todas las pruebas pasaron\n", 0))

    def test_my_strlen(self):
        for text, expected in {"": "0", "a": "1", "hola": "4", "dos palabras": "12",
                               "año": "4", "ñandú": "7"}.items():
            with self.subTest(text=text):
                self.assertEqual(call("l", text), expected)

    def test_my_strcmp_equal(self):
        for text in ["", "a", "ana", "año"]:
            with self.subTest(text=text):
                self.assertEqual(call("c", text, text), "0")

    def test_my_strcmp_order(self):
        # Solo importa el signo. Cada par (a, b) cumple que a va antes que b.
        pairs = [("ana", "ano"), ("Z", "a"), ("an", "ana"), ("", "a"), ("a", "b"), ("z", "ñ")]
        for a, b in pairs:
            with self.subTest(a=a, b=b):
                self.assertLess(int(call("c", a, b)), 0)
                self.assertGreater(int(call("c", b, a)), 0)

    def test_to_upper(self):
        cases = {"hola": "HOLA", "": "", "Ya ES": "YA ES", "lima 2026!": "LIMA 2026!",
                 "az{`": "AZ{`", "año": "AñO"}
        for text, expected in cases.items():
            with self.subTest(text=text):
                self.assertEqual(call("u", text), expected)

    def test_is_palindrome(self):
        cases = {"": "1", "a": "1", "aa": "1", "ab": "0", "abba": "1", "reconocer": "1",
                 "abca": "0", "Ana": "0", "ana ana": "1", "a a": "1", "ana a": "0",
                 "aña": "0"}
        for text, expected in cases.items():
            with self.subTest(text=text):
                self.assertEqual(call("p", text), expected)

    def test_is_palindrome_text(self):
        cases = {"": "1", "   ": "1", "a": "1", " a ": "1", "Ana": "1", "ANA": "1", "ab": "0",
                 "Anita lava la tina": "1", "  a b  B A": "1", "Hola": "0", "a b c": "0"}
        for text, expected in cases.items():
            with self.subTest(text=text):
                self.assertEqual(call("t", text), expected)

    def test_greet(self):
        cases = {"Ana\n": "Hola, Ana\n", "Ana": "Hola, Ana\n", "Ana Li\n": "Hola, Ana Li\n",
                 "Rosaura\n": "Hola, Rosaura\n", "Alejandra\n": "Hola, Alejand\n"}
        for text, expected in cases.items():
            with self.subTest(text=text):
                self.assertEqual(run("greet", text), (expected, 0))

    def test_greet_no_name(self):
        for text in ["", "\n"]:
            with self.subTest(text=text):
                self.assertEqual(run("greet", text), ("Entrada inválida\n", 1))


if __name__ == "__main__":
    unittest.main()
test_notes.pyDescargar test_notes.py
"""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 pathlib
import re
import subprocess
import unittest

HERE = pathlib.Path(__file__).parent
ROOT = HERE.parent.parent
NOTES = ROOT / "notes" / "week-10.md"
SLIDES = ROOT / "slides" / "week-10.tex"
# Antes de un bloque con direcciones o con comportamiento indefinido. De él solo se
# exigen estas cadenas estables, si el bloque las muestra.
MACHINE_DEPENDENT = "<!-- depende de la corrida -->"
STABLE = ["AddressSanitizer: stack-buffer-overflow"]
# En esta salida, solo las direcciones cambian.
ADDRESSES = re.compile(r"0x[0-9a-f]+")


def normalize(text):
    # Según el idioma del entorno, gcc usa comillas tipográficas o rectas.
    return ADDRESSES.sub("0x", text.replace("‘", "'").replace("’", "'"))


def run_shell(command):
    done = subprocess.run(
        ["bash", "-c", command], cwd=HERE, encoding="utf-8", errors="replace",
        stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=60,
    )
    return normalize(done.stdout)


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, depende_de_la_corrida) de cada bloque de los apuntes."""
    blocks, current, flagged, previous = [], None, False, ""
    for raw in NOTES.read_text(encoding="utf-8").splitlines():
        line = re.sub(r"^> ?", "", raw)
        if current is None:
            if line.strip() == "```" + language:
                current, flagged = [], previous.strip() == MACHINE_DEPENDENT
            elif line.strip():
                previous = line
        elif line.strip() == "```":
            blocks.append((current, flagged))
            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()
    for source in list(HERE.glob("*.c")) + list((HERE / "errors").glob("*.c")):
        lines.update(line.strip() for line in source.read_text(encoding="utf-8").splitlines())
    return lines


class NotesAreTrue(unittest.TestCase):
    @unittest.skipUnless(NOTES.exists(), "faltan los apuntes")
    def test_terminal_blocks_in_notes(self):
        checked = 0
        for lines, flagged in note_blocks("text"):
            if not lines or not lines[0].startswith("$ "):
                continue
            for command, expected in split_commands(lines):
                actual = run_shell(command).rstrip("\n")
                checked += 1
                with self.subTest(command=command):
                    if flagged:
                        for token in STABLE:
                            if any(token in line for line in expected):
                                self.assertIn(token, actual)
                    else:
                        lines = [line.rstrip() for line in actual.split("\n")]
                        self.assertEqual("\n".join(lines), "\n".join(expected))
        self.assertGreater(checked, 20)

    @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")
    def test_notes_show_the_report_and_flag_undefined_behavior(self):
        text = NOTES.read_text(encoding="utf-8")
        self.assertIn("WRITE of size 10", text)
        self.assertIn("en esta corrida", " ".join(text.split()))


class SlidesAreTrue(unittest.TestCase):
    @unittest.skipUnless(SLIDES.exists(), "faltan las diapositivas")
    def test_terminal_blocks_in_slides(self):
        for block in slide_blocks("terminal|salidalarga"):
            for command, expected in split_commands(block):
                actual = run_shell(command).splitlines()
                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.

lab/greet.cDescargar lab/greet.c
// Laboratorio, parte 3: este programa tiene un buffer overflow a propósito.
//
// Primero provóquenlo: escriban un nombre de más de 7 letras y lean el informe de
// -fsanitize=address. Después corríjanlo con fgets, para que el programa:
//
//   - lea una línea completa, también con espacios, en name;
//   - quite el salto de línea, si está;
//   - escriba "Hola, " y el nombre, y termine con return 0;
//   - guarde solo los primeros 7 bytes si el nombre no cabe, y escriba eso;
//   - escriba "Entrada inválida" y termine con return 1 si no hay ninguna línea o si
//     la línea está vacía.
//
// No cambien SIZE. Pruébenlo con make test-lab.
#include <stdio.h>

#define SIZE 8

int main(void) {
    char name[SIZE];

    if (scanf("%s", name) != 1) {
        printf("Entrada inválida\n");
        return 1;
    }
    printf("Hola, %s\n", name);
    return 0;
}
lab/my_strings.cDescargar lab/my_strings.c
// Laboratorio, partes 1 y 2: funciones de cadenas, sin string.h.
//
// Escriban cada función según su comentario. Borren la línea (void) de cada una:
// solo está para que el archivo compile sin advertencias mientras tanto.
// Las cadenas pueden traer bytes de UTF-8, como los dos de la ñ.
//
// En main hay una prueba de cada función. Agreguen más con assert: casos normales
// y bordes, como la cadena vacía.
//
// Pruébenlo con make test-lab.
#include <assert.h>
#include <stdio.h>

// Devuelve cuántos bytes hay antes del '\0'.
int my_strlen(const char s[]) {
    (void) s;
    return 0;
}

// Compara a y b byte por byte, hasta la primera diferencia o hasta el final.
// Devuelve 0 si son iguales. Devuelve un número negativo si a va antes que b, y uno
// positivo si a va después. Una cadena que es el inicio de otra va antes.
// Comparen los bytes como unsigned char, para que el resultado no dependa de la máquina.
int my_strcmp(const char a[], const char b[]) {
    (void) a;
    (void) b;
    return 0;
}

// Convierte a mayúscula las letras de la 'a' a la 'z', en el mismo arreglo.
// Todos los demás bytes quedan igual.
void to_upper(char s[]) {
    (void) s;
}

// Devuelve 1 si s se lee igual byte por byte de izquierda a derecha y de derecha a
// izquierda, y 0 si no. Sin ninguna excepción: "Ana" no es palíndromo, porque
// 'A' y 'a' son bytes distintos. La cadena vacía sí lo es.
int is_palindrome(const char s[]) {
    (void) s;
    return 0;
}

// Igual que is_palindrome, pero ignora los espacios y no distingue entre mayúsculas
// y minúsculas de la 'a' a la 'z'. "Anita lava la tina" sí es palíndromo.
// s no se puede cambiar. Una cadena sin nada más que espacios sí es palíndromo.
int is_palindrome_text(const char s[]) {
    (void) s;
    return 0;
}

int main(void) {
    assert(my_strlen("hola") == 4);
    assert(my_strcmp("ana", "ana") == 0);

    char word[] = "hola";
    to_upper(word);
    assert(word[0] == 'H');

    assert(is_palindrome("reconocer") == 1);
    assert(is_palindrome_text("Anita lava la tina") == 1);

    printf("Todas las pruebas pasaron\n");
    return 0;
}

Compilación

Cómo se compilan y ejecutan los ejemplos.

MakefileDescargar Makefile
# Semana 10: arreglos en funciones y cadenas. Se usa dentro del entorno del curso.
#
#   make           compila los programas de la clase en build/
#   make test      corre las pruebas de los ejemplos y de los apuntes
#   make test-lab  prueba su laboratorio (lab/*.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.
#
# check_strings.c es parte de las pruebas: llama a las funciones de my_strings.c.
# Para enlazarlos, el main de my_strings.c se compila con otro nombre. Se compila con
# -fsigned-char para que make test-lab dé lo mismo en todas las máquinas.

CC = gcc
CFLAGS = -Wall -Wextra -std=c17 -g -fsanitize=address,undefined
BUILD = build
PROGRAMS = sum_array bonus same_address const_param vector string_bytes cut_string \
           my_strlen count_lowercase read_word read_line read_name age_name copy_text

.PHONY: all lab test test-lab clean

all: $(PROGRAMS:%=$(BUILD)/%)

$(BUILD)/%: %.c | $(BUILD)
	$(CC) $(CFLAGS) $< -o $@

$(BUILD) $(BUILD)/lab $(BUILD)/solution:
	mkdir -p $@

$(BUILD)/lab/% : lab/%.c | $(BUILD)/lab
	$(CC) $(CFLAGS) $< -o $@

$(BUILD)/solution/% : solution/%.c | $(BUILD)/solution
	$(CC) $(CFLAGS) $< -o $@

$(BUILD)/lab/check_strings: check_strings.c lab/my_strings.c | $(BUILD)/lab
	$(CC) $(CFLAGS) -fsigned-char -Dmain=student_main -c lab/my_strings.c -o $(BUILD)/lab/my_strings.o
	$(CC) $(CFLAGS) -fsigned-char check_strings.c $(BUILD)/lab/my_strings.o -o $@

$(BUILD)/solution/check_strings: check_strings.c solution/my_strings.c | $(BUILD)/solution
	$(CC) $(CFLAGS) -fsigned-char -Dmain=student_main -c solution/my_strings.c -o $(BUILD)/solution/my_strings.o
	$(CC) $(CFLAGS) -fsigned-char check_strings.c $(BUILD)/solution/my_strings.o -o $@

LAB_FILES = my_strings greet check_strings

lab: $(LAB_FILES:%=$(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_FILES:%=$(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__