Semana 1 — De C a C++
Objetivos
- Explicar de qué trata el curso: elegir cómo organizar los datos según lo que el programa necesita hacer.
- Traducir lo que ya saben de C a C++: referencias, clases y métodos.
- Usar
std::stringystd::vectorsin manejar la memoria a mano. - Compilar con
g++ -std=c++20y escribir pruebas conassert.
Teoría — Presentación · De C a C++: referencias, clases, std::string, std::vector y pruebas
Lectura: Apuntes, semana 1.
Apoyo: Open Data Structures, secs. 1.1 (The Need for Efficiency) y 1.2 (Interfaces).
Temas:
- De qué trata el curso. Un mismo programa puede tardar un segundo o un día según cómo guarde sus datos.
- Lo que no cambia desde C: tipos, control, funciones, punteros, compilar y enlazar.
- Referencias:
int&yconst std::string&. En qué se parecen a un puntero y en qué no. Cuándo pasar por valor y cuándo por referencia constante. - De
structcon funciones sueltas a clase: campos privados, métodos, constructor y métodosconst. El tipo opaco de C ahora lo daprivate. std::stringystd::vectorcomo primeros usuarios de lo que construiremos.forpor rango yauto.- Namespaces. Por qué en este curso se escribe
std::y nousing namespace std. - Pruebas con
asserty compilación con-Wall -Wextray sanitizers. - Error común: copiar sin querer un
std::vectorgrande al pasarlo por valor.
Laboratorio — Portar un módulo de C a C++
Sesión 1:
- Abrir el entorno y correr
env/check.sh. Compilar y correr un programa corto en C++ con elMakefiledado. - Leer un módulo de C dado, con tipo opaco y su encabezado: un registro de notas de estudiantes.
- Portarlo a una clase
GradeBook:std::stringen vez dechar *,std::vectoren vez demalloc, métodos en vez de funciones con prefijo.
Sesión 2:
4. Pasar las pruebas assert del módulo original a la clase, y agregar casos borde.
5. Agregar average y best, marcándolos const, y comprobar que el compilador rechaza un método const que modifica un campo.
6. Comparar las dos versiones: qué memoria maneja cada una a mano y cuántas líneas desaparecieron.
Preguntas de repaso
- ¿Qué diferencia hay entre
void f(std::vector<int> v)yvoid f(const std::vector<int>& v)? ¿Cuál conviene para leer un arreglo grande, y por qué? - ¿Puede una referencia quedar sin apuntar a nada, como un puntero nulo? ¿Puede cambiar a qué se refiere después de crearse?
- ¿Qué garantiza un método marcado
const? - ¿Qué parte del tipo opaco de C reemplaza
private?
Código de la semana
Compilar y probar dentro del contenedor del curso con make y make test.
Ejemplos (25)Errores (20)Pruebas (4)Laboratorio (12)Compilación (1)
Ejemplos
Los programas que usan los apuntes y la clase.
// Un programa escrito como en C. g++ lo compila sin cambiar casi nada.
#include <cstdio>
struct point {
int x;
int y;
};
int manhattan(const point *p) {
int dx = p->x < 0 ? -p->x : p->x;
int dy = p->y < 0 ? -p->y : p->y;
return dx + dy;
}
int main() {
point origin = {0, 0};
point target = {3, -4};
int values[] = {manhattan(&origin), manhattan(&target)};
for (int i = 0; i < 2; i++) {
std::printf("%d\n", values[i]);
}
return 0;
}// Las formas de const con punteros y referencias. Se leen de derecha a izquierda.
#include <cstddef>
#include <iostream>
#include <string>
std::size_t count_letter(const std::string& text, char letter) {
std::size_t found = 0;
for (char c : text) {
if (c == letter) {
found++;
}
}
return found;
}
int main() {
int a = 1;
int b = 2;
const int *to_const = &a; // no se puede escribir *to_const
to_const = &b; // sí se puede cambiar el puntero
int *const fixed = &a; // no se puede cambiar el puntero
*fixed = 5; // sí se puede escribir a través de él
const int& view = a; // no se puede escribir a través de view
a = 7; // a sí cambia, y view lo ve
std::cout << *to_const << " " << *fixed << " " << view << "\n";
// Una literal crea una std::string temporal; const& se puede atar a ella.
std::cout << count_letter("banana", 'a') << "\n";
return 0;
}// const protege los campos del objeto, no lo que hay al otro lado de un puntero.
#include <iostream>
class Remote {
public:
explicit Remote(int *target) : target_(target) {}
// Compila: no cambia target_, solo el entero al que apunta.
void reset() const {
*target_ = 0;
}
private:
int *target_;
};
int main() {
int counter = 5;
const Remote remote(&counter);
remote.reset();
std::cout << counter << "\n";
return 0;
}// Suma n elementos llamando a una función en cada vuelta. Uso: copy_in_loop n copy|ref
#include <cstdlib>
#include <iostream>
#include <string>
#include <vector>
#include "value_at.h"
int main(int argc, char *argv[]) {
if (argc != 3) {
std::cerr << "uso: copy_in_loop n copy|ref\n";
return 1;
}
std::size_t n = std::strtoul(argv[1], nullptr, 10);
std::string mode = argv[2];
std::vector<int> values(n, 1);
long long sum = 0;
long long copied = 0;
for (std::size_t i = 0; i < n; i++) {
if (mode == "copy") {
sum += value_at_copy(values, i);
copied += values.size();
} else {
sum += value_at_ref(values, i);
}
}
std::cout << "suma = " << sum << ", elementos copiados = " << copied << "\n";
return 0;
}// ¿Pasar un std::vector crea otro bloque? Se comprueba comparando direcciones.
#include <iostream>
#include <vector>
void by_value(std::vector<int> values, const int *original) {
values[0] = 99;
std::cout << "por valor: mismo bloque = " << (values.data() == original)
<< ", values[0] = " << values[0] << "\n";
}
void by_const_reference(const std::vector<int>& values, const int *original) {
std::cout << "por const&: mismo bloque = " << (values.data() == original)
<< ", values[0] = " << values[0] << "\n";
}
void by_reference(std::vector<int>& values, const int *original) {
values[0] = 99;
std::cout << "por referencia: mismo bloque = " << (values.data() == original)
<< ", values[0] = " << values[0] << "\n";
}
int main() {
std::vector<int> numbers = {1, 2, 3};
const int *block = numbers.data();
by_value(numbers, block);
std::cout << "en main: numbers[0] = " << numbers[0] << "\n";
by_const_reference(numbers, block);
std::cout << "en main: numbers[0] = " << numbers[0] << "\n";
by_reference(numbers, block);
std::cout << "en main: numbers[0] = " << numbers[0] << "\n";
return 0;
}// Una clase con constructor, campos privados, métodos const y una referencia devuelta.
#include <cassert>
#include <iostream>
#include <string>
class Course {
public:
Course(const std::string& name, int credits) : name_(name), credits_(credits) {
assert(credits > 0);
}
void enroll() {
enrolled_++;
}
int enrolled() const {
return enrolled_;
}
int credits() const {
return credits_;
}
// Devuelve el nombre sin copiarlo. La referencia es del objeto y vive lo que él.
const std::string& name() const {
return name_;
}
// private es por clase: este método puede ver los campos de other.
bool same_credits_as(const Course& other) const {
return credits_ == other.credits_;
}
private:
std::string name_;
int credits_;
int enrolled_ = 0;
};
void print_summary(const Course& course) {
std::cout << course.name() << ": " << course.credits() << " créditos, "
<< course.enrolled() << " matriculados\n";
}
int main() {
Course data("Estructuras de Datos", 4);
Course math("Matemática Discreta", 4);
data.enroll();
data.enroll();
print_summary(data);
print_summary(math);
std::cout << "mismos créditos: " << data.same_credits_as(math) << "\n";
return 0;
}// Primer programa en C++: lo mismo que un printf, con std::cout.
#include <iostream>
int main() {
int students = 32;
std::cout << "Hola, Estructuras de Datos\n";
std::cout << "Somos " << students << " estudiantes\n";
return 0;
}// Busca los n códigos de una lista de n estudiantes y cuenta las comparaciones.
// Uso: lookup n linear|binary
#include <cstdlib>
#include <iostream>
#include <string>
#include <vector>
// Devuelve el índice de code, o -1. Suma a comparisons cada elemento que mira.
long long linear_search(const std::vector<int>& codes, int code, long long& comparisons) {
long long size = static_cast<long long>(codes.size());
for (long long i = 0; i < size; i++) {
comparisons++;
if (codes[i] == code) {
return i;
}
}
return -1;
}
// Igual, pero exige que codes esté ordenado.
long long binary_search(const std::vector<int>& codes, int code, long long& comparisons) {
long long low = 0;
long long high = static_cast<long long>(codes.size()) - 1;
while (low <= high) {
long long middle = low + (high - low) / 2;
comparisons++;
if (codes[middle] == code) {
return middle;
}
if (codes[middle] < code) {
low = middle + 1;
} else {
high = middle - 1;
}
}
return -1;
}
int main(int argc, char *argv[]) {
if (argc != 3) {
std::cerr << "uso: lookup n linear|binary\n";
return 1;
}
int n = std::atoi(argv[1]);
std::string mode = argv[2];
std::vector<int> codes;
for (int i = 0; i < n; i++) {
codes.push_back(2 * i);
}
long long comparisons = 0;
long long found = 0;
for (int code : codes) {
long long index = mode == "linear" ? linear_search(codes, code, comparisons)
: binary_search(codes, code, comparisons);
if (index >= 0) {
found++;
}
}
std::cout << "n = " << n << ", encontrados = " << found
<< ", comparaciones = " << comparisons << "\n";
return 0;
}"""Mide los programas de la semana: mediana de 5 corridas, compilados con -O2 y sin
sanitizers. Los tiempos dependen de la máquina; lo que importa es cómo crecen."""
import statistics
import subprocess
import time
REPETITIONS = 5
def median_seconds(command):
times = []
for _ in range(REPETITIONS):
start = time.perf_counter()
subprocess.run(command, check=True, stdout=subprocess.DEVNULL)
times.append(time.perf_counter() - start)
return statistics.median(times)
def table(title, program, mode, sizes):
print(title)
previous = None
for n in sizes:
seconds = median_seconds([program, str(n), mode])
growth = "" if previous is None else f" x{seconds / previous:.1f}"
print(f"{n:>10} {seconds:>9.3f} s{growth}")
previous = seconds
table("Búsqueda lineal", "build/fast/lookup", "linear", [10000, 20000, 40000, 80000])
table("Búsqueda binaria", "build/fast/lookup", "binary", [5000000, 10000000, 20000000])
table("Por copia", "build/fast/copy_in_loop", "copy", [20000, 40000, 80000])
table("Por referencia constante", "build/fast/copy_in_loop", "ref", [20000, 40000, 80000])
table("Por referencia constante, n mucho mayor", "build/fast/copy_in_loop", "ref",
[10000000, 20000000, 40000000])// Dos funciones con el mismo nombre, cada una en su namespace.
#include <iostream>
namespace circle {
double area(double radius) {
return 3.14159 * radius * radius;
}
} // namespace circle
namespace square {
double area(double side) {
return side * side;
}
} // namespace square
int main() {
std::cout << circle::area(2.0) << "\n";
std::cout << square::area(2.0) << "\n";
return 0;
}// for por rango con copia, con referencia y con auto.
#include <iostream>
#include <string>
#include <vector>
int main() {
std::vector<int> grades = {10, 15};
// x es una copia de cada elemento.
for (int x : grades) {
x = x + 1;
std::cout << x << " ";
}
std::cout << "/ " << grades[0] << " " << grades[1] << "\n";
// x es otro nombre del elemento.
for (int& x : grades) {
x = x + 1;
}
std::cout << grades[0] << " " << grades[1] << "\n";
std::vector<std::string> names = {"Ana", "Luis"};
for (const std::string& name : names) {
std::cout << name << " ";
}
std::cout << "\n";
// auto deduce el tipo; sin & sigue siendo una copia.
for (auto name : names) {
name += "!";
}
std::cout << names[0] << "\n";
for (auto& name : names) {
name += "!";
}
std::cout << names[0] << "\n";
auto size = names.size();
const auto& first = names[0];
std::cout << size << " " << first << "\n";
return 0;
}// Una referencia es otro nombre para un objeto que ya existe.
#include <iostream>
int main() {
int grade = 14;
int& alias = grade;
alias = 15;
std::cout << "grade = " << grade << "\n";
std::cout << "misma dirección: " << (&alias == &grade) << "\n";
// Esto no cambia a quién se refiere alias: copia 20 dentro de grade.
int other = 20;
alias = other;
other = 0;
std::cout << "grade = " << grade << ", other = " << other << "\n";
return 0;
}// Quien usa una clase conoce su tamaño, aunque los campos sean privados.
#include <iostream>
#include "tally.h"
int main() {
Tally local;
local.add(4);
std::cout << "sizeof(Tally) = " << sizeof(Tally) << "\n";
std::cout << "cuenta = " << local.count() << "\n";
return 0;
}// Diferencias pequeñas con C: bool, nullptr, static_cast y división entera.
#include <iostream>
bool is_passing(int grade) {
return grade >= 11;
}
int main() {
int total = 29;
int count = 2;
const int *missing = nullptr;
std::cout << is_passing(14) << " " << is_passing(9) << "\n";
std::cout << (missing == nullptr) << "\n";
std::cout << total / count << "\n";
std::cout << static_cast<double>(total) / count << "\n";
return 0;
}// std::string: lo que en C hacían strlen, strcpy, strcat y strcmp.
#include <cstddef>
#include <cstdio>
#include <iostream>
#include <string>
int main() {
std::string first = "Ana";
std::string full = first + " Quispe";
std::string copy = full;
copy[0] = 'E';
std::cout << full << " / " << copy << "\n";
std::cout << "largo: " << full.size() << "\n";
std::cout << "iguales: " << (full == copy) << "\n";
std::cout << "ordenadas: " << (full < copy) << "\n";
std::size_t space = full.find(' ');
std::cout << "apellido: " << full.substr(space + 1) << "\n";
if (full.find('z') == std::string::npos) {
std::cout << "no hay z\n";
}
full += ", sección A";
std::cout << full << "\n";
// Para una función de C que pide const char *.
std::printf("%s\n", first.c_str());
return 0;
}// El mismo intercambio con punteros, como en C, y con referencias.
#include <iostream>
void swap_pointer(int *a, int *b) {
int saved = *a;
*a = *b;
*b = saved;
}
void swap_reference(int& a, int& b) {
int saved = a;
a = b;
b = saved;
}
int main() {
int x = 1;
int y = 2;
swap_pointer(&x, &y);
std::cout << x << " " << y << "\n";
swap_reference(x, y);
std::cout << x << " " << y << "\n";
return 0;
}// Uso de la clase: no hay puntero, no se revisa memoria y no se libera a mano.
#include <iostream>
#include "tally.h"
int main() {
Tally tally;
tally.add(14);
tally.add(9);
tally.add(17);
std::cout << tally.count() << " valores, suma " << tally.sum() << ", mayor " << tally.max() << "\n";
return 0;
}#include "tally.h"
#include <cassert>
void Tally::add(int value) {
values_.push_back(value);
}
std::size_t Tally::count() const {
return values_.size();
}
int Tally::sum() const {
int total = 0;
for (int value : values_) {
total += value;
}
return total;
}
int Tally::max() const {
assert(!values_.empty());
int largest = values_[0];
for (int value : values_) {
if (value > largest) {
largest = value;
}
}
return largest;
}// Clase Tally: acumula enteros y responde cuántos son, cuánto suman y cuál es el mayor.
#ifndef TALLY_H
#define TALLY_H
#include <cstddef>
#include <vector>
class Tally {
public:
// Agrega un valor.
void add(int value);
// Devuelve cuántos valores se agregaron.
std::size_t count() const;
// Devuelve la suma de los valores. Con cero valores, es 0.
int sum() const;
// Devuelve el mayor valor. Exige: count() > 0.
int max() const;
private:
std::vector<int> values_;
};
#endif#include "value_at.h"
int value_at_copy(std::vector<int> values, std::size_t index) {
return values[index];
}
int value_at_ref(const std::vector<int>& values, std::size_t index) {
return values[index];
}// Dos formas de leer un elemento. Están en otro archivo para que el compilador no
// pueda quitar la copia al optimizar.
#ifndef VALUE_AT_H
#define VALUE_AT_H
#include <cstddef>
#include <vector>
int value_at_copy(std::vector<int> values, std::size_t index);
int value_at_ref(const std::vector<int>& values, std::size_t index);
#endif// std::vector: lo que en C era un arreglo con malloc y realloc.
#include <iostream>
#include <string>
#include <vector>
struct Score {
std::string name;
int points;
};
int main() {
std::vector<int> grades;
grades.push_back(14);
grades.push_back(9);
grades.push_back(17);
std::cout << "tamaño: " << grades.size() << "\n";
std::cout << "último: " << grades.back() << "\n";
grades[1] = 11;
grades.erase(grades.begin());
std::cout << "primero tras borrar: " << grades[0] << "\n";
std::vector<int> zeros(3, 0);
std::vector<int> primes = {2, 3, 5};
std::vector<int> other = primes;
other[0] = 7;
std::cout << primes[0] << " " << other[0] << "\n";
std::cout << "iguales: " << (primes == other) << "\n";
std::cout << "ceros: " << zeros.size() << ", vacío: " << zeros.empty() << "\n";
std::vector<Score> table;
table.push_back({"Ana", 18});
table.push_back({"Luis", 12});
std::cout << table[1].name << " tiene " << table[1].points << "\n";
grades.clear();
std::cout << "tras clear: " << grades.size() << "\n";
return 0;
}// Uso del módulo en C: quien lo usa crea, revisa y libera.
#include <stdio.h>
#include "tally.h"
int main(void) {
Tally *tally = tally_new();
if (tally == NULL) {
return 1;
}
int ok = tally_add(tally, 14) && tally_add(tally, 9) && tally_add(tally, 17);
if (!ok) {
tally_free(tally);
return 1;
}
printf("%d valores, suma %d, mayor %d\n", tally_count(tally), tally_sum(tally), tally_max(tally));
tally_free(tally);
return 0;
}// Implementación con un arreglo que crece: guarda todos los valores.
#include "tally.h"
#include <assert.h>
#include <stdlib.h>
struct tally {
int *values;
int count;
int capacity;
};
Tally *tally_new(void) {
Tally *tally = malloc(sizeof *tally);
if (tally == NULL) {
return NULL;
}
tally->values = NULL;
tally->count = 0;
tally->capacity = 0;
return tally;
}
void tally_free(Tally *tally) {
if (tally == NULL) {
return;
}
free(tally->values);
free(tally);
}
int tally_add(Tally *tally, int value) {
if (tally->count == tally->capacity) {
int capacity = tally->capacity == 0 ? 4 : 2 * tally->capacity;
int *values = realloc(tally->values, capacity * sizeof *values);
if (values == NULL) {
return 0;
}
tally->values = values;
tally->capacity = capacity;
}
tally->values[tally->count] = value;
tally->count++;
return 1;
}
int tally_count(const Tally *tally) {
return tally->count;
}
int tally_sum(const Tally *tally) {
int sum = 0;
for (int i = 0; i < tally->count; i++) {
sum += tally->values[i];
}
return sum;
}
int tally_max(const Tally *tally) {
assert(tally->count > 0);
int largest = tally->values[0];
for (int i = 1; i < tally->count; i++) {
if (tally->values[i] > largest) {
largest = tally->values[i];
}
}
return largest;
}// Módulo Tally: acumula enteros y responde cuántos son, cuánto suman y cuál es el mayor.
// Es un tipo opaco: quien lo usa solo tiene punteros y solo llama a estas funciones.
#ifndef TALLY_H
#define TALLY_H
typedef struct tally Tally;
// Crea un acumulador vacío. Devuelve NULL si no hay memoria.
// Owner: quien llama, que debe liberarlo con tally_free.
Tally *tally_new(void);
// Libera el acumulador. Acepta NULL. Después de esto, tally ya no se puede usar.
void tally_free(Tally *tally);
// Agrega un valor. Devuelve 1 si pudo y 0 si no hay memoria; en ese caso, no cambia nada.
int tally_add(Tally *tally, int value);
// Devuelve cuántos valores se agregaron.
int tally_count(const Tally *tally);
// Devuelve la suma de los valores. Con cero valores, es 0.
int tally_sum(const Tally *tally);
// Devuelve el mayor valor. Exige: tally_count(tally) > 0.
int tally_max(const Tally *tally);
#endifErrores
Ejemplos con errores a propósito: no compilan, fallan o dan un resultado incorrecto. Los apuntes explican cada uno.
// Con NDEBUG, assert desaparece, y con él lo que hacía adentro.
#include <cassert>
#include <iostream>
#include <vector>
int main() {
std::vector<int> values = {1, 2, 3};
values.pop_back();
assert((values.push_back(9), values.size() == 3));
std::cout << values.size() << "\n";
return 0;
}// Un método const no puede modificar un campo.
class Counter {
public:
int next() const {
value_++;
return value_;
}
private:
int value_ = 0;
};
int main() {
Counter counter;
return counter.next();
}// A un objeto const solo se le pueden pedir métodos const.
#include "../tally.h"
int total_after_add(const Tally& tally) {
tally.add(1);
return tally.sum();
}
int main() {
Tally tally;
return total_after_add(tally);
}// Una referencia constante no permite escribir.
void reset(const int& value) {
value = 0;
}
int main() {
int grade = 14;
reset(grade);
return grade;
}// Los campos se inicializan en el orden en que están declarados, no en el de la lista.
#include <iostream>
class Range {
public:
explicit Range(int width) : high_(low_ + width), low_(0) {}
int high() const {
return high_;
}
private:
int low_;
int high_;
};
int main() {
Range range(5);
std::cout << range.high() << "\n";
return 0;
}// Sin & en el for por rango, se modifica una copia.
#include <iostream>
#include <vector>
struct Score {
int points;
};
int main() {
std::vector<Score> table = {{10}, {15}};
for (auto score : table) {
score.points += 1;
}
std::cout << table[0].points << " " << table[1].points << "\n";
return 0;
}// Válido en C, no en C++: class es una palabra reservada.
int main() {
int class = 3;
return class;
}// En C++, void * no se convierte solo a otro tipo de puntero.
#include <cstdlib>
int main() {
int *block = std::malloc(4 * sizeof *block);
std::free(block);
return 0;
}// Al declarar un constructor, el compilador ya no genera el que no recibe nada.
class Range {
public:
Range(int low, int high) : low_(low), high_(high) {}
int width() const {
return high_ - low_;
}
private:
int low_;
int high_;
};
int main() {
Range range;
return range.width();
}// Quien usa la clase no puede tocar sus campos privados.
#include "../tally.h"
int main() {
Tally tally;
tally.values_.clear();
return 0;
}// Una referencia sin const no se ata a un valor temporal.
#include <cstddef>
#include <string>
std::size_t length(std::string& text) {
return text.size();
}
int main() {
return static_cast<int>(length("hola"));
}// Una referencia debe atarse a un objeto al crearse.
int main() {
int& alias;
return 0;
}// Referencia colgante: la local muere al retornar.
#include <iostream>
int& next_code(int code) {
int result = code + 1;
return result;
}
int main() {
int& code = next_code(10);
std::cout << code << "\n";
return 0;
}// size() no tiene signo: comparar con int da una advertencia.
#include <iostream>
#include <vector>
int main() {
std::vector<int> values = {1, 2, 3};
for (int i = 0; i < values.size(); i++) {
std::cout << values[i] << "\n";
}
return 0;
}// size() - 1 con un vector vacío da un número enorme, no -1.
#include <iostream>
#include <vector>
int main() {
std::vector<int> empty;
std::cout << empty.size() - 1 << "\n";
return 0;
}// Con using namespace std, un nombre propio choca con uno de la biblioteca.
#include <algorithm>
#include <iostream>
#include <vector>
using namespace std;
int count = 0;
int main() {
vector<int> values = {1, 2, 3};
for (int value : values) {
count += value;
}
cout << count << "\n";
return 0;
}// at revisa el índice.
#include <iostream>
#include <vector>
int main() {
std::vector<int> values = {1, 2, 3};
std::cout << values.at(3) << "\n";
return 0;
}// operator[] no revisa el índice.
#include <iostream>
#include <vector>
int main() {
std::vector<int> values = {1, 2, 3};
std::cout << values[3] << "\n";
return 0;
}// Fuera de size(), pero dentro del bloque: ASan no lo ve.
#include <iostream>
#include <vector>
int main() {
std::vector<int> values;
values.reserve(8);
values.push_back(1);
values[5] = 42;
std::cout << "size = " << values.size() << ", values[5] = " << values[5] << "\n";
return 0;
}// Con paréntesis vacíos, esto declara una función, no un objeto.
#include "../tally.h"
int main() {
Tally tally();
tally.add(1);
return 0;
}Pruebas
Comprueban lo que afirman los apuntes; se ejecutan con make test.
// Pruebas de Tally: casos dirigidos y operaciones al azar con semilla fija.
#include <algorithm>
#include <cassert>
#include <iostream>
#include <numeric>
#include <random>
#include <vector>
#include "tally.h"
void test_starts_empty() {
Tally tally;
assert(tally.count() == 0);
assert(tally.sum() == 0);
}
void test_one_value() {
Tally tally;
tally.add(-3);
assert(tally.count() == 1);
assert(tally.sum() == -3);
assert(tally.max() == -3);
}
void test_read_through_const_reference(const Tally& tally) {
assert(tally.count() == 3);
assert(tally.max() == 17);
}
void test_several_values() {
Tally tally;
tally.add(14);
tally.add(9);
tally.add(17);
assert(tally.sum() == 40);
test_read_through_const_reference(tally);
}
// Compara contra un std::vector, que hace de referencia.
void test_against_reference() {
std::mt19937 random(2026);
Tally tally;
std::vector<int> reference;
for (int step = 0; step < 2000; step++) {
int value = static_cast<int>(random() % 201) - 100;
tally.add(value);
reference.push_back(value);
assert(tally.count() == reference.size());
assert(tally.sum() == std::accumulate(reference.begin(), reference.end(), 0));
assert(tally.max() == *std::max_element(reference.begin(), reference.end()));
}
}
int main() {
test_starts_empty();
test_one_value();
test_several_values();
test_against_reference();
std::cout << "tally: todas las pruebas pasaron\n";
return 0;
}"""Comprueba los programas de la clase y lo que los apuntes dicen de sus errores."""
import pathlib
import re
import subprocess
import tempfile
import unittest
HERE = pathlib.Path(__file__).parent
BUILD = HERE / "build"
CXX = ["g++", "-Wall", "-Wextra", "-std=c++20", "-g", "-fsanitize=address,undefined"]
C_MODULE = HERE / "lab" / "c"
# Programa de errors/ que no compila, y parte del mensaje de g++.
COMPILE_ERRORS = {
"malloc_no_cast": "invalid conversion from 'void*' to 'int*'",
"keyword_name": "expected primary-expression before 'int'",
"using_std_collision": "reference to 'count' is ambiguous",
"ref_uninitialized": "'alias' declared as reference but not initialized",
"ref_to_temporary": "cannot bind non-const lvalue reference",
"const_ref_write": "assignment of read-only reference 'value'",
"private_access": "is private within this context",
"const_method_writes": "increment of member 'Counter::value_' in read-only object",
"const_object_call": "discards qualifiers",
"no_default_ctor": "no matching function for call to 'Range::Range()'",
"vexing_parse": "which is of non-class type 'Tally()'",
}
# Programa de errors/ que compila con una advertencia, y su opción.
WARNINGS = {
"return_local_ref": "-Wreturn-local-addr",
"init_order": "-Wreorder",
"sign_compare": "-Wsign-compare",
}
# Programa de errors/ que falla al correr, y lo que debe decir.
RUNTIME_ERRORS = {
"vector_index": "heap-buffer-overflow",
"vector_at": "std::out_of_range",
}
def compile_error(name):
done = subprocess.run(CXX + ["-I.", "-c", f"errors/{name}.cpp", "-o", "/dev/null"], cwd=HERE, text=True,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=120)
return done.returncode, done.stdout.replace("‘", "'").replace("’", "'")
class Programs(unittest.TestCase):
def output(self, name, *args):
done = subprocess.run([str(BUILD / name), *args], cwd=HERE, text=True, capture_output=True, timeout=60)
self.assertEqual(done.returncode, 0, done.stderr)
return done.stdout
def test_every_program_builds_without_warnings(self):
with tempfile.TemporaryDirectory() as temp:
for source in sorted(HERE.glob("*.cpp")):
done = subprocess.run(CXX + ["-c", str(source), "-o", f"{temp}/program.o"], cwd=HERE, text=True,
capture_output=True, timeout=120)
with self.subTest(source=source.name):
self.assertEqual((done.returncode, done.stderr), (0, ""))
def test_tally_tests_pass(self):
self.assertIn("todas las pruebas pasaron", self.output("tally_tests"))
def test_both_tallies_say_the_same(self):
self.assertEqual(self.output("tally_demo"), self.output("tally_c_demo"))
def test_lookup_counts(self):
# Lineal: 1 + 2 + ... + n exactamente.
for n in (1, 10, 1000):
text = self.output("lookup", str(n), "linear")
self.assertIn(f"comparaciones = {n * (n + 1) // 2}", text)
# Binaria: como mucho floor(log2(n)) + 1 por búsqueda.
text = self.output("lookup", "1000", "binary")
count = int(re.search(r"comparaciones = (\d+)", text).group(1))
self.assertLessEqual(count, 1000 * 10)
def test_copy_counts(self):
self.assertIn("elementos copiados = 1000000", self.output("copy_in_loop", "1000", "copy"))
self.assertIn("elementos copiados = 0", self.output("copy_in_loop", "1000", "ref"))
def test_only_by_value_makes_another_block(self):
lines = self.output("copy_or_not").splitlines()
self.assertIn("mismo bloque = 0", lines[0])
self.assertIn("mismo bloque = 1", lines[2])
self.assertIn("mismo bloque = 1", lines[4])
class DeliberateErrors(unittest.TestCase):
def test_compile_errors(self):
for name, message in COMPILE_ERRORS.items():
with self.subTest(program=name):
code, text = compile_error(name)
self.assertNotEqual(code, 0)
self.assertIn(message, text)
def test_warnings(self):
for name, option in WARNINGS.items():
with self.subTest(program=name):
code, text = compile_error(name)
self.assertEqual(code, 0)
self.assertIn(option, text)
def test_runtime_errors(self):
with tempfile.TemporaryDirectory() as temp:
for name, message in RUNTIME_ERRORS.items():
with self.subTest(program=name):
program = f"{temp}/{name}"
subprocess.run(CXX + [f"errors/{name}.cpp", "-o", program], cwd=HERE, check=True, timeout=120)
done = subprocess.run([program], text=True, capture_output=True, timeout=60)
self.assertNotEqual(done.returncode, 0)
self.assertIn(message, done.stderr)
class GivenCModule(unittest.TestCase):
"""El módulo en C del laboratorio es de verdad: compila limpio, pasa sus pruebas y no tiene fugas."""
def test_compiles_and_passes_with_sanitizers(self):
with tempfile.TemporaryDirectory() as temp:
program = f"{temp}/tests"
done = subprocess.run(["gcc", "-Wall", "-Wextra", "-std=c17", "-g", "-fsanitize=address,undefined",
str(C_MODULE / "test_grade_book.c"), str(C_MODULE / "grade_book.c"), "-o", program],
text=True, capture_output=True, timeout=120)
self.assertEqual((done.returncode, done.stderr), (0, ""))
run = subprocess.run([program], text=True, capture_output=True, timeout=60)
self.assertEqual(run.returncode, 0, run.stderr)
def test_clean_under_valgrind(self):
with tempfile.TemporaryDirectory() as temp:
program = f"{temp}/tests"
subprocess.run(["gcc", "-Wall", "-Wextra", "-std=c17", "-g", "-O0", str(C_MODULE / "test_grade_book.c"),
str(C_MODULE / "grade_book.c"), "-o", program], check=True, timeout=120)
run = subprocess.run(["valgrind", "--leak-check=full", "--error-exitcode=9", program], text=True,
capture_output=True, timeout=120)
self.assertEqual(run.returncode, 0, run.stderr)
self.assertIn("All heap blocks were freed", run.stderr)
class SolutionComparison(unittest.TestCase):
@unittest.skipUnless((HERE / "solution").exists(), "falta la solución")
def test_numbers_match_the_files(self):
def lines(path):
return len((HERE / path).read_text(encoding="utf-8").splitlines())
text = (HERE / "solution" / "comparison.txt").read_text(encoding="utf-8")
h_c, c_c = lines("lab/c/grade_book.h"), lines("lab/c/grade_book.c")
h_cpp, c_cpp = lines("solution/grade_book.h"), lines("solution/grade_book.cpp")
self.assertIn(f"{h_c + c_c} ({h_c} + {c_c})", text)
self.assertIn(f"{h_cpp + c_cpp} ({h_cpp} + {c_cpp})", text)
if __name__ == "__main__":
unittest.main()"""Pruebas del laboratorio. LAB_DIR dice dónde está la clase: lab o solution.
Cada prueba compila con sanitizers lo que necesita y lo corre: una fuga o un acceso
inválido la hace fallar, aunque el resultado parezca correcto.
"""
import os
import pathlib
import random
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
GIVEN = HERE / "lab"
CXX_FLAGS = ["g++", "-Wall", "-Wextra", "-std=c++20", "-g", "-fsanitize=address,undefined"]
C_FLAGS = ["gcc", "-Wall", "-Wextra", "-std=c17", "-g", "-fsanitize=address,undefined"]
# Mínimo de assert en sus pruebas: los 38 del módulo en C que se pueden pasar a la clase,
# más al menos tres de casos borde.
MIN_ASSERTS = 41
# Lo que ya no debe aparecer en la clase.
MANUAL_MEMORY = r"\b(malloc|calloc|realloc|free|strdup|strcpy|strcmp|strlen|new|delete)\b|\bchar\s*\*"
def strip_comments(text):
return re.sub(r"//[^\n]*", "", text)
def build(command, output):
done = subprocess.run(command + ["-o", str(output)], cwd=HERE, text=True,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=120)
return done.returncode, done.stdout
def run(program, stdin=""):
return subprocess.run([str(program)], cwd=HERE, input=stdin, text=True,
stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=60)
def random_commands(seed, steps):
"""Una secuencia de órdenes con semilla fija, para comparar las dos versiones."""
generator = random.Random(seed)
lines = []
for _ in range(steps):
name = f"e{generator.randrange(25)}"
grade = generator.randrange(-2, 23)
command = generator.choice(["add", "add", "set", "get", "remove", "list"])
if command in ("add", "set"):
lines.append(f"{command} {name} {grade}")
elif command in ("get", "remove"):
lines.append(f"{command} {name}")
else:
lines.append("list")
return "\n".join(lines) + "\n"
@unittest.skipUnless(SOURCE.exists(), f"no existe {LAB_DIR}/")
class LabCase(unittest.TestCase):
def setUp(self):
self.folder = tempfile.TemporaryDirectory()
self.addCleanup(self.folder.cleanup)
self.temp = pathlib.Path(self.folder.name)
def compile_class(self, main_source):
"""Compila main_source con la clase. Falla la prueba si no compila o si da advertencias."""
program = self.temp / "program"
code, messages = build(CXX_FLAGS + [f"-I{SOURCE}", str(main_source),
str(SOURCE / "grade_book.cpp")], program)
self.assertEqual(code, 0, f"no compila:\n{messages}")
self.assertEqual(messages.strip(), "", "hay advertencias del compilador")
return program
def assert_runs(self, program):
done = run(program)
self.assertEqual(done.returncode, 0, done.stdout + done.stderr)
class Step1Environment(LabCase):
def test_hello(self):
program = self.temp / "hello"
code, messages = build(CXX_FLAGS + [str(GIVEN / "hello.cpp")], program)
self.assertEqual((code, messages.strip()), (0, ""))
self.assertEqual(run(program).stdout, "entorno: ok\ncompilador: ok\nmake: ok\n")
class Step2GivenModule(LabCase):
def test_c_module_compiles_and_passes(self):
program = self.temp / "tests_c"
code, messages = build(C_FLAGS + [str(GIVEN / "c" / "test_grade_book.c"),
str(GIVEN / "c" / "grade_book.c")], program)
self.assertEqual((code, messages.strip()), (0, ""))
self.assert_runs(program)
class Step3Port(LabCase):
def test_public_tests(self):
self.assert_runs(self.compile_class(GIVEN / "tests" / "public_port.cpp"))
def test_name_at_checks_its_contract(self):
source = self.temp / "out_of_range.cpp"
source.write_text('#include "grade_book.h"\nint main() {\n GradeBook book;\n'
' return static_cast<int>(book.name_at(0).size());\n}\n')
done = run(self.compile_class(source))
self.assertNotEqual(done.returncode, 0, "name_at fuera de rango debe detenerse con assert")
def test_same_behavior_as_the_c_module(self):
driver_c = self.temp / "driver_c"
code, messages = build(C_FLAGS + [str(GIVEN / "c" / "driver.c"),
str(GIVEN / "c" / "grade_book.c")], driver_c)
self.assertEqual((code, messages.strip()), (0, ""))
driver = self.compile_class(GIVEN / "tests" / "driver.cpp")
for seed in (1, 2, 3):
commands = random_commands(seed, 2000)
expected = run(driver_c, commands)
actual = run(driver, commands)
self.assertEqual(expected.returncode, 0, expected.stderr)
self.assertEqual(actual.returncode, 0, actual.stderr)
self.assertEqual(actual.stdout, expected.stdout, f"semilla {seed}")
def test_no_manual_memory(self):
for name in ("grade_book.h", "grade_book.cpp"):
text = strip_comments((SOURCE / name).read_text(encoding="utf-8"))
found = re.search(MANUAL_MEMORY, text)
self.assertIsNone(found, f"{name}: {found.group(0) if found else ''}")
def test_uses_string_and_vector(self):
text = strip_comments((SOURCE / "grade_book.h").read_text(encoding="utf-8"))
self.assertIn("std::vector", text)
self.assertNotIn("placeholder_", text)
class Step4OwnTests(LabCase):
def test_own_tests_pass(self):
self.assert_runs(self.compile_class(SOURCE / "test_grade_book.cpp"))
def test_own_tests_cover_the_c_tests_and_more(self):
text = strip_comments((SOURCE / "test_grade_book.cpp").read_text(encoding="utf-8"))
count = len(re.findall(r"\bassert\(", text))
self.assertGreaterEqual(count, MIN_ASSERTS, f"hay {count} assert")
class Step5Stats(LabCase):
def test_public_tests(self):
self.assert_runs(self.compile_class(GIVEN / "tests" / "public_stats.cpp"))
def test_add_is_not_const(self):
source = self.temp / "const_add.cpp"
source.write_text('#include "grade_book.h"\nvoid f(const GradeBook& book) { book.add("a", 1); }\n')
command = CXX_FLAGS + [f"-I{SOURCE}", "-c", str(source)]
code, _ = build(command, self.temp / "const_add.o")
self.assertNotEqual(code, 0, "un GradeBook const no debe aceptar add")
class Step6Comparison(LabCase):
def test_comparison_is_filled_in(self):
text = (SOURCE / "comparison.txt").read_text(encoding="utf-8")
self.assertNotIn("PENDIENTE", text)
self.assertRegex(text, "read-only|discards qualifiers", "falta la línea del error del compilador")
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 pathlib
import platform
import re
import subprocess
import unittest
try:
import pty
except ImportError:
pty = None
HERE = pathlib.Path(__file__).parent
ROOT = HERE.parent.parent
NOTES = ROOT / "notes" / "week-01.md"
SLIDES = ROOT / "slides" / "week-01.tex"
# Antes de un bloque cuya salida cambia con el sistema. No se compara.
SYSTEM_DEPENDENT = "<!-- depende del sistema -->"
# Antes de un informe del compilador o de un sanitizer. Puede omitir líneas, pero no
# cambiarlas ni reordenarlas. Las direcciones y los números de proceso no se comparan.
REPORT = "<!-- informe recortado -->"
# Los informes se capturaron en esta arquitectura: en otra cambian líneas de las bibliotecas.
REPORT_MACHINE = "aarch64"
def normalize(text):
# Según el idioma del entorno, g++ 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",
stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=120,
)
return normalize(done.stdout)
def run_in_terminal(command):
"""Corre el comando en una terminal de verdad: así se intercala la salida del programa
con el informe, igual que cuando ustedes lo corren."""
master, slave = pty.openpty()
process = subprocess.Popen(
["bash", "-c", command], cwd=HERE, stdin=subprocess.DEVNULL, stdout=slave, stderr=slave,
)
os.close(slave)
data = b""
while True:
try:
chunk = os.read(master, 4096)
except OSError:
break
if not chunk:
break
data += chunk
os.close(master)
process.wait(timeout=120)
text = data.decode("utf-8", errors="replace").replace("\r", "")
return normalize(re.sub(r"\x1b\[[0-9;]*[mK]", "", text))
def clean_lines(text):
return [line.rstrip() for line in text.splitlines()]
def stable(line):
"""Quita de una línea de informe lo que cambia en cada corrida."""
line = re.sub(r"0x[0-9a-f]+", "ADDR", line.strip())
return re.sub(r"==\d+==", "==N==", line)
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 = []
for folder in (HERE, HERE / "errors", HERE / "tally_c", HERE / "lab", HERE / "lab" / "c", HERE / "lab" / "tests"):
for pattern in ("*.cpp", "*.h", "*.c"):
sources += folder.glob(pattern)
for source in sources:
lines.update(line.strip() for line in source.read_text(encoding="utf-8").splitlines())
return lines
class ReportCheck(unittest.TestCase):
def assert_in_order(self, command, expected, actual):
"""Las líneas esperadas aparecen en la salida real, en orden. '...' marca un recorte."""
wanted = [stable(line) for line in expected if line.strip() and line.strip() != "..."]
real = [stable(line) for line in actual]
position = 0
for line in wanted:
with self.subTest(command=command, line=line):
found = [i for i in range(position, len(real)) if real[i] == line]
self.assertTrue(found, f"no aparece en la salida real: {line}")
if found:
position = found[0] + 1
class NotesAreTrue(ReportCheck):
@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 == REPORT:
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, 15)
@unittest.skipUnless(NOTES.exists(), "faltan los apuntes")
@unittest.skipUnless(pty and platform.machine() == REPORT_MACHINE, "los informes son de aarch64")
def test_reports_in_notes(self):
checked = 0
for lines, mark in note_blocks("text"):
if mark != REPORT:
continue
for command, expected in split_commands(lines):
actual = clean_lines(run_in_terminal(command))
self.assert_in_order(command, expected, actual)
checked += 1
self.assertGreater(checked, 8)
@unittest.skipUnless(NOTES.exists(), "faltan los apuntes")
def test_code_blocks_in_notes_exist(self):
known = known_code_lines()
for language in ("c", "cpp"):
for lines, _ in note_blocks(language):
for line in lines:
if line.strip() and line.strip() != "...":
with self.subTest(line=line):
self.assertIn(line.strip(), known)
class SlidesAreTrue(ReportCheck):
@unittest.skipUnless(SLIDES.exists(), "faltan las diapositivas")
def test_program_output_in_slides(self):
for block in slide_blocks("terminal|salida"):
for command, expected in split_commands(block):
self.assert_in_order(command, expected, clean_lines(run_shell(command)))
# Algunos informes usan programas que compiló la prueba de los apuntes, que corre antes.
@unittest.skipUnless(SLIDES.exists(), "faltan las diapositivas")
@unittest.skipUnless(pty and platform.machine() == REPORT_MACHINE, "los informes son de aarch64")
def test_reports_in_slides(self):
for block in slide_blocks("informe"):
for command, expected in split_commands(block):
self.assert_in_order(command, expected, clean_lines(run_in_terminal(command)))
@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.
Paso 6: comparen las dos versiones. Corran `make compare` y reemplacen cada línea pendiente.
1. Líneas del módulo en C (grade_book.h + grade_book.c): PENDIENTE
2. Líneas de la clase (grade_book.h + grade_book.cpp): PENDIENTE
3. ¿Qué memoria maneja a mano cada versión? ¿Quién la libera en cada una?
PENDIENTE
4. ¿Qué chequeos de "no hay memoria" del módulo en C desaparecieron, y por qué?
PENDIENTE
5. Paso 5: hagan que average modifique un campo, compilen y peguen aquí la línea
del error. Después deshagan el cambio.
PENDIENTE// Paso 3: porten aquí c/grade_book.c. Al implementar cada método, pónganle nombre a
// sus parámetros. Nada de malloc, free ni char *.
#include "grade_book.h"
bool GradeBook::add(const std::string&, int) {
return false;
}
bool GradeBook::set_grade(const std::string&, int) {
return false;
}
int GradeBook::grade_of(const std::string&) const {
return -2;
}
bool GradeBook::remove(const std::string&) {
return false;
}
std::size_t GradeBook::size() const {
return 99;
}
const std::string& GradeBook::name_at(std::size_t) const {
return placeholder_;
}
// Paso 5: definan aquí average y best.// Clase GradeBook: registro de notas de estudiantes. Notas de 0 a 20.
// Es el módulo de c/grade_book.h, como clase. Mismas reglas, sin memoria a mano.
#ifndef GRADE_BOOK_H
#define GRADE_BOOK_H
#include <cstddef>
#include <string>
class GradeBook {
public:
// Agrega un estudiante al final. Devuelve true si pudo, y false si el nombre está
// vacío o repetido, o si la nota no es válida. En ese caso, no cambia nada.
bool add(const std::string& name, int grade);
// Cambia la nota. Devuelve true si pudo, y false si no está o si la nota no es válida.
bool set_grade(const std::string& name, int grade);
// Devuelve la nota de un estudiante, o -1 si no está.
int grade_of(const std::string& name) const;
// Borra un estudiante, sin cambiar el orden de los demás. Devuelve true si estaba.
bool remove(const std::string& name);
// Devuelve cuántos estudiantes hay.
std::size_t size() const;
// Devuelve el nombre en la posición index. Exige: index < size().
// La referencia es del registro: deja de servir si se modifica el registro.
const std::string& name_at(std::size_t index) const;
// Paso 5: declaren aquí average y best.
private:
// Paso 3: reemplacen este campo por los datos del registro.
std::string placeholder_;
};
#endif// Paso 1: comprueben que el entorno compila y corre C++.
#include <iostream>
#include <string>
#include <vector>
int main() {
std::vector<std::string> steps = {"entorno", "compilador", "make"};
for (const std::string& step : steps) {
std::cout << step << ": ok\n";
}
return 0;
}// Paso 4: pasen aquí las pruebas de c/test_grade_book.c y agreguen casos borde propios.
// Ya está la primera, para que vean la forma.
#include <cassert>
#include <iostream>
#include "grade_book.h"
void test_starts_empty() {
GradeBook book;
assert(book.size() == 0);
assert(book.grade_of("Ana") == -1);
}
// Falta pasar: test_add_and_get, test_rejects_invalid, test_set, test_remove_keeps_order,
// test_keeps_its_own_copy_of_the_name y test_grows_past_initial_capacity.
// Casos borde propios: agreguen al menos tres.
int main() {
test_starts_empty();
std::cout << "mis pruebas: todas pasaron\n";
return 0;
}// Lee órdenes de la entrada y las aplica al registro. Se compara con ../tests/driver.cpp.
#include <stdio.h>
#include <string.h>
#include "grade_book.h"
int main(void) {
GradeBook *book = grade_book_new();
if (book == NULL) {
return 1;
}
char command[16];
char name[64];
int grade;
while (scanf("%15s", command) == 1) {
if (strcmp(command, "add") == 0 && scanf("%63s %d", name, &grade) == 2) {
printf("%d\n", grade_book_add(book, name, grade));
} else if (strcmp(command, "set") == 0 && scanf("%63s %d", name, &grade) == 2) {
printf("%d\n", grade_book_set(book, name, grade));
} else if (strcmp(command, "get") == 0 && scanf("%63s", name) == 1) {
printf("%d\n", grade_book_get(book, name));
} else if (strcmp(command, "remove") == 0 && scanf("%63s", name) == 1) {
printf("%d\n", grade_book_remove(book, name));
} else if (strcmp(command, "list") == 0) {
printf("%d:", grade_book_size(book));
for (int i = 0; i < grade_book_size(book); i++) {
printf(" %s", grade_book_name_at(book, i));
}
printf("\n");
}
}
grade_book_free(book);
return 0;
}#include "grade_book.h"
#include <stdlib.h>
#include <string.h>
#define INITIAL_CAPACITY 4
#define MAX_GRADE 20
struct entry {
char *name;
int grade;
};
struct grade_book {
struct entry *entries;
int size;
int capacity;
};
// Devuelve una copia de text en un bloque nuevo, o NULL si no hay memoria.
static char *copy_text(const char *text) {
char *copy = malloc(strlen(text) + 1);
if (copy != NULL) {
strcpy(copy, text);
}
return copy;
}
static int is_valid_grade(int grade) {
return grade >= 0 && grade <= MAX_GRADE;
}
// Devuelve la posición de name, o -1 si no está.
static int find_index(const GradeBook *book, const char *name) {
for (int i = 0; i < book->size; i++) {
if (strcmp(book->entries[i].name, name) == 0) {
return i;
}
}
return -1;
}
// Asegura un lugar libre. Devuelve 0 si no hay memoria.
static int make_room(GradeBook *book) {
if (book->size < book->capacity) {
return 1;
}
int capacity = 2 * book->capacity;
struct entry *entries = realloc(book->entries, capacity * sizeof *entries);
if (entries == NULL) {
return 0;
}
book->entries = entries;
book->capacity = capacity;
return 1;
}
GradeBook *grade_book_new(void) {
GradeBook *book = malloc(sizeof *book);
if (book == NULL) {
return NULL;
}
book->entries = malloc(INITIAL_CAPACITY * sizeof *book->entries);
if (book->entries == NULL) {
free(book);
return NULL;
}
book->size = 0;
book->capacity = INITIAL_CAPACITY;
return book;
}
void grade_book_free(GradeBook *book) {
if (book == NULL) {
return;
}
for (int i = 0; i < book->size; i++) {
free(book->entries[i].name);
}
free(book->entries);
free(book);
}
int grade_book_add(GradeBook *book, const char *name, int grade) {
if (name[0] == '\0' || !is_valid_grade(grade) || find_index(book, name) >= 0) {
return 0;
}
if (!make_room(book)) {
return 0;
}
char *copy = copy_text(name);
if (copy == NULL) {
return 0;
}
book->entries[book->size].name = copy;
book->entries[book->size].grade = grade;
book->size++;
return 1;
}
int grade_book_set(GradeBook *book, const char *name, int grade) {
int index = find_index(book, name);
if (index < 0 || !is_valid_grade(grade)) {
return 0;
}
book->entries[index].grade = grade;
return 1;
}
int grade_book_get(const GradeBook *book, const char *name) {
int index = find_index(book, name);
return index < 0 ? -1 : book->entries[index].grade;
}
int grade_book_remove(GradeBook *book, const char *name) {
int index = find_index(book, name);
if (index < 0) {
return 0;
}
free(book->entries[index].name);
for (int i = index; i < book->size - 1; i++) {
book->entries[i] = book->entries[i + 1];
}
book->size--;
return 1;
}
int grade_book_size(const GradeBook *book) {
return book->size;
}
const char *grade_book_name_at(const GradeBook *book, int index) {
if (index < 0 || index >= book->size) {
return NULL;
}
return book->entries[index].name;
}// Módulo GradeBook: registro de notas de estudiantes. Notas de 0 a 20.
// Es un tipo opaco: quien lo usa solo tiene punteros y solo llama a estas funciones.
#ifndef GRADE_BOOK_H
#define GRADE_BOOK_H
typedef struct grade_book GradeBook;
// Crea un registro vacío. Devuelve NULL si no hay memoria.
// Owner: quien llama, que debe liberarlo con grade_book_free.
GradeBook *grade_book_new(void);
// Libera el registro y todos sus nombres. Acepta NULL.
void grade_book_free(GradeBook *book);
// Agrega un estudiante al final, con una copia de name. Devuelve 1 si pudo, y 0 si el
// nombre está vacío o repetido, si la nota no es válida o si no hay memoria.
// En ese caso, no cambia nada.
int grade_book_add(GradeBook *book, const char *name, int grade);
// Cambia la nota de un estudiante. Devuelve 1 si pudo, y 0 si no está o si la nota no es válida.
int grade_book_set(GradeBook *book, const char *name, int grade);
// Devuelve la nota de un estudiante, o -1 si no está.
int grade_book_get(const GradeBook *book, const char *name);
// Borra un estudiante, sin cambiar el orden de los demás. Devuelve 1 si estaba y 0 si no.
int grade_book_remove(GradeBook *book, const char *name);
// Devuelve cuántos estudiantes hay.
int grade_book_size(const GradeBook *book);
// Devuelve el nombre en la posición index, o NULL si esa posición no existe.
// Owner: el registro. Deja de servir al borrar ese estudiante o al liberar el registro.
const char *grade_book_name_at(const GradeBook *book, int index);
#endif// Pruebas del módulo en C. En el paso 4 las pasan a lab/test_grade_book.cpp.
#include <assert.h>
#include <stdio.h>
#include <string.h>
#include "grade_book.h"
static void test_starts_empty(void) {
GradeBook *book = grade_book_new();
assert(book != NULL);
assert(grade_book_size(book) == 0);
assert(grade_book_get(book, "Ana") == -1);
assert(grade_book_name_at(book, 0) == NULL);
grade_book_free(book);
}
static void test_add_and_get(void) {
GradeBook *book = grade_book_new();
assert(grade_book_add(book, "Ana", 14) == 1);
assert(grade_book_add(book, "Luis", 9) == 1);
assert(grade_book_size(book) == 2);
assert(grade_book_get(book, "Ana") == 14);
assert(grade_book_get(book, "Luis") == 9);
assert(strcmp(grade_book_name_at(book, 1), "Luis") == 0);
grade_book_free(book);
}
static void test_rejects_invalid(void) {
GradeBook *book = grade_book_new();
assert(grade_book_add(book, "Ana", -1) == 0);
assert(grade_book_add(book, "Ana", 21) == 0);
assert(grade_book_add(book, "", 10) == 0);
assert(grade_book_size(book) == 0);
assert(grade_book_add(book, "Ana", 0) == 1);
assert(grade_book_add(book, "Ana", 20) == 0);
assert(grade_book_get(book, "Ana") == 0);
assert(grade_book_size(book) == 1);
grade_book_free(book);
}
static void test_set(void) {
GradeBook *book = grade_book_new();
assert(grade_book_add(book, "Ana", 14) == 1);
assert(grade_book_set(book, "Ana", 20) == 1);
assert(grade_book_get(book, "Ana") == 20);
assert(grade_book_set(book, "Ana", 21) == 0);
assert(grade_book_get(book, "Ana") == 20);
assert(grade_book_set(book, "Luis", 10) == 0);
assert(grade_book_size(book) == 1);
grade_book_free(book);
}
static void test_remove_keeps_order(void) {
GradeBook *book = grade_book_new();
assert(grade_book_add(book, "Ana", 14) == 1);
assert(grade_book_add(book, "Luis", 9) == 1);
assert(grade_book_add(book, "Rosa", 17) == 1);
assert(grade_book_remove(book, "Luis") == 1);
assert(grade_book_remove(book, "Luis") == 0);
assert(grade_book_size(book) == 2);
assert(strcmp(grade_book_name_at(book, 0), "Ana") == 0);
assert(strcmp(grade_book_name_at(book, 1), "Rosa") == 0);
grade_book_free(book);
}
static void test_keeps_its_own_copy_of_the_name(void) {
GradeBook *book = grade_book_new();
char name[] = "Ana";
assert(grade_book_add(book, name, 14) == 1);
name[0] = 'E';
assert(grade_book_get(book, "Ana") == 14);
assert(grade_book_get(book, "Ena") == -1);
grade_book_free(book);
}
static void test_grows_past_initial_capacity(void) {
GradeBook *book = grade_book_new();
for (int i = 0; i < 100; i++) {
char name[16];
snprintf(name, sizeof name, "e%d", i);
assert(grade_book_add(book, name, i % 21) == 1);
}
assert(grade_book_size(book) == 100);
assert(grade_book_get(book, "e0") == 0);
assert(grade_book_get(book, "e99") == 99 % 21);
grade_book_free(book);
}
int main(void) {
test_starts_empty();
test_add_and_get();
test_rejects_invalid();
test_set();
test_remove_keeps_order();
test_keeps_its_own_copy_of_the_name();
test_grows_past_initial_capacity();
printf("módulo en C: todas las pruebas pasaron\n");
return 0;
}// Lee órdenes de la entrada y las aplica al registro. Se compara con ../c/driver.c.
#include <cstddef>
#include <iostream>
#include <string>
#include "grade_book.h"
int main() {
GradeBook book;
std::string command;
while (std::cin >> command) {
std::string name;
int grade = 0;
if (command == "add") {
std::cin >> name >> grade;
std::cout << (book.add(name, grade) ? 1 : 0) << "\n";
} else if (command == "set") {
std::cin >> name >> grade;
std::cout << (book.set_grade(name, grade) ? 1 : 0) << "\n";
} else if (command == "get") {
std::cin >> name;
std::cout << book.grade_of(name) << "\n";
} else if (command == "remove") {
std::cin >> name;
std::cout << (book.remove(name) ? 1 : 0) << "\n";
} else if (command == "list") {
std::cout << book.size() << ":";
for (std::size_t i = 0; i < book.size(); i++) {
std::cout << " " << book.name_at(i);
}
std::cout << "\n";
}
}
return 0;
}// Pruebas públicas de los pasos 3 y 4: la clase hace lo mismo que el módulo en C.
#include <cassert>
#include <cstddef>
#include <iostream>
#include <random>
#include <string>
#include <utility>
#include <vector>
#include "grade_book.h"
void test_starts_empty() {
const GradeBook book{};
assert(book.size() == 0);
assert(book.grade_of("Ana") == -1);
}
void test_add_and_get() {
GradeBook book;
assert(book.add("Ana", 14));
assert(book.add("Luis", 9));
assert(book.size() == 2);
assert(book.grade_of("Ana") == 14);
assert(book.grade_of("Luis") == 9);
assert(book.name_at(0) == "Ana");
assert(book.name_at(1) == "Luis");
}
void test_rejects_invalid() {
GradeBook book;
assert(!book.add("Ana", -1));
assert(!book.add("Ana", 21));
assert(!book.add("", 10));
assert(book.size() == 0);
assert(book.add("Ana", 0));
assert(book.add("Rosa", 20));
assert(!book.add("Ana", 20));
assert(book.grade_of("Ana") == 0);
assert(book.size() == 2);
}
void test_set_grade() {
GradeBook book;
assert(book.add("Ana", 14));
assert(book.set_grade("Ana", 20));
assert(book.grade_of("Ana") == 20);
assert(!book.set_grade("Ana", 21));
assert(!book.set_grade("Ana", -1));
assert(book.grade_of("Ana") == 20);
assert(!book.set_grade("Luis", 10));
assert(book.size() == 1);
}
void test_remove_keeps_order() {
GradeBook book;
assert(book.add("Ana", 14));
assert(book.add("Luis", 9));
assert(book.add("Rosa", 17));
assert(book.remove("Luis"));
assert(!book.remove("Luis"));
assert(book.size() == 2);
assert(book.name_at(0) == "Ana");
assert(book.name_at(1) == "Rosa");
assert(book.remove("Rosa"));
assert(book.remove("Ana"));
assert(book.size() == 0);
assert(book.add("Luis", 3));
assert(book.grade_of("Luis") == 3);
}
void test_keeps_its_own_copy_of_the_name() {
GradeBook book;
std::string name = "Ana";
assert(book.add(name, 14));
name[0] = 'E';
assert(book.grade_of("Ana") == 14);
assert(book.grade_of("Ena") == -1);
}
void test_copy_is_independent() {
GradeBook book;
assert(book.add("Ana", 14));
GradeBook other = book;
assert(other.set_grade("Ana", 5));
assert(other.add("Luis", 9));
assert(book.grade_of("Ana") == 14);
assert(book.size() == 1);
}
// Devuelve la posición de name en la referencia, o reference.size() si no está.
std::size_t find_in(const std::vector<std::pair<std::string, int>>& reference, const std::string& name) {
std::size_t i = 0;
while (i < reference.size() && reference[i].first != name) {
i++;
}
return i;
}
// Operaciones al azar, con semilla fija, comparadas contra un std::vector de pares.
void test_against_reference() {
std::mt19937 random(2026);
GradeBook book;
std::vector<std::pair<std::string, int>> reference;
for (int step = 0; step < 3000; step++) {
std::string name = "e" + std::to_string(random() % 30);
int grade = static_cast<int>(random() % 25) - 2;
bool valid = grade >= 0 && grade <= 20;
std::size_t place = find_in(reference, name);
bool present = place < reference.size();
switch (random() % 4) {
case 0:
assert(book.add(name, grade) == (valid && !present));
if (valid && !present) {
reference.push_back({name, grade});
}
break;
case 1:
assert(book.set_grade(name, grade) == (valid && present));
if (valid && present) {
reference[place].second = grade;
}
break;
case 2:
assert(book.remove(name) == present);
if (present) {
reference.erase(reference.begin() + place);
}
break;
default:
assert(book.grade_of(name) == (present ? reference[place].second : -1));
}
assert(book.size() == reference.size());
for (std::size_t i = 0; i < reference.size(); i++) {
assert(book.name_at(i) == reference[i].first);
}
}
}
int main() {
test_starts_empty();
test_add_and_get();
test_rejects_invalid();
test_set_grade();
test_remove_keeps_order();
test_keeps_its_own_copy_of_the_name();
test_copy_is_independent();
test_against_reference();
std::cout << "pasos 3 y 4: todas las pruebas pasaron\n";
return 0;
}// Pruebas públicas del paso 5. No compila hasta que declaren average y best.
#include <cassert>
#include <cmath>
#include <iostream>
#include <string>
#include "grade_book.h"
// Recibe const&: si average o best no son const, esto no compila.
double read_average(const GradeBook& book) {
return book.average();
}
std::string read_best(const GradeBook& book) {
return book.best();
}
bool close_to(double a, double b) {
return std::fabs(a - b) < 1e-9;
}
void test_empty() {
const GradeBook book{};
assert(close_to(read_average(book), 0.0));
assert(read_best(book).empty());
}
void test_average_is_not_integer_division() {
GradeBook book;
assert(book.add("Ana", 14));
assert(book.add("Luis", 15));
assert(close_to(read_average(book), 14.5));
}
void test_best() {
GradeBook book;
assert(book.add("Ana", 0));
assert(read_best(book) == "Ana");
assert(book.add("Luis", 12));
assert(book.add("Rosa", 17));
assert(book.add("Omar", 3));
assert(read_best(book) == "Rosa");
assert(close_to(read_average(book), 8.0));
}
void test_tie_keeps_the_first() {
GradeBook book;
assert(book.add("Ana", 18));
assert(book.add("Luis", 18));
assert(read_best(book) == "Ana");
}
void test_follows_changes() {
GradeBook book;
assert(book.add("Ana", 10));
assert(book.add("Luis", 20));
assert(book.remove("Luis"));
assert(read_best(book) == "Ana");
assert(close_to(read_average(book), 10.0));
assert(book.set_grade("Ana", 4));
assert(close_to(read_average(book), 4.0));
assert(book.remove("Ana"));
assert(close_to(read_average(book), 0.0));
assert(read_best(book).empty());
}
int main() {
test_empty();
test_average_is_not_integer_division();
test_best();
test_tie_keeps_the_first();
test_follows_changes();
std::cout << "paso 5: todas las pruebas pasaron\n";
return 0;
}Compilación
Cómo se compilan y ejecutan los ejemplos.
# Semana 1: de C a C++. Se usa dentro del entorno del curso.
#
# make compila los programas de la clase en build/
# make measure compila con -O2 y mide tiempos (tarda cerca de medio minuto)
# 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; falla hasta que lo terminen
# make compare cuenta líneas y memoria a mano de las dos versiones (paso 6)
# 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
CXX = g++
CFLAGS = -Wall -Wextra -std=c17 -g -fsanitize=address,undefined
CXXFLAGS = -Wall -Wextra -std=c++20 -g -fsanitize=address,undefined
FASTFLAGS = -Wall -Wextra -std=c++20 -O2
BUILD = build
LAB_DIR ?= lab
PROGRAMS = hello c_style small_changes namespaces reference_basics swap_both const_forms \
copy_or_not const_shallow course strings vectors range_for lookup
C_MODULE = lab/c/grade_book.c lab/c/grade_book.h
CPP_MODULE = $(LAB_DIR)/grade_book.cpp $(LAB_DIR)/grade_book.h
.PHONY: all lab measure test test-lab compare clean
all: $(PROGRAMS:%=$(BUILD)/%) $(BUILD)/copy_in_loop $(BUILD)/sizes \
$(BUILD)/tally_demo $(BUILD)/tally_tests $(BUILD)/tally_c_demo
$(BUILD)/%: %.cpp | $(BUILD)
$(CXX) $(CXXFLAGS) $< -o $@
$(BUILD)/copy_in_loop: copy_in_loop.cpp value_at.cpp value_at.h | $(BUILD)
$(CXX) $(CXXFLAGS) copy_in_loop.cpp value_at.cpp -o $@
$(BUILD)/sizes: sizes.cpp tally.cpp tally.h | $(BUILD)
$(CXX) $(CXXFLAGS) sizes.cpp tally.cpp -o $@
$(BUILD)/tally_demo: tally_demo.cpp tally.cpp tally.h | $(BUILD)
$(CXX) $(CXXFLAGS) tally_demo.cpp tally.cpp -o $@
$(BUILD)/tally_tests: tally_tests.cpp tally.cpp tally.h | $(BUILD)
$(CXX) $(CXXFLAGS) tally_tests.cpp tally.cpp -o $@
$(BUILD)/tally_c_demo: tally_c/tally_demo.c tally_c/tally.c tally_c/tally.h | $(BUILD)
$(CC) $(CFLAGS) tally_c/tally_demo.c tally_c/tally.c -o $@
# Para medir: -O2 y sin sanitizers.
$(BUILD)/fast/lookup: lookup.cpp | $(BUILD)/fast
$(CXX) $(FASTFLAGS) $< -o $@
$(BUILD)/fast/copy_in_loop: copy_in_loop.cpp value_at.cpp value_at.h | $(BUILD)/fast
$(CXX) $(FASTFLAGS) copy_in_loop.cpp value_at.cpp -o $@
measure: $(BUILD)/fast/lookup $(BUILD)/fast/copy_in_loop
python3 measure.py
# Laboratorio: el módulo en C que se les da, y la clase que escriben.
$(BUILD)/lab/hello: lab/hello.cpp | $(BUILD)/lab
$(CXX) $(CXXFLAGS) $< -o $@
$(BUILD)/lab/test_grade_book_c: lab/c/test_grade_book.c $(C_MODULE) | $(BUILD)/lab
$(CC) $(CFLAGS) lab/c/test_grade_book.c lab/c/grade_book.c -o $@
$(BUILD)/lab/driver_c: lab/c/driver.c $(C_MODULE) | $(BUILD)/lab
$(CC) $(CFLAGS) lab/c/driver.c lab/c/grade_book.c -o $@
$(BUILD)/lab/test_grade_book: $(LAB_DIR)/test_grade_book.cpp $(CPP_MODULE) | $(BUILD)/lab
$(CXX) $(CXXFLAGS) -I$(LAB_DIR) $(LAB_DIR)/test_grade_book.cpp $(LAB_DIR)/grade_book.cpp -o $@
$(BUILD)/lab/public_port: lab/tests/public_port.cpp $(CPP_MODULE) | $(BUILD)/lab
$(CXX) $(CXXFLAGS) -I$(LAB_DIR) lab/tests/public_port.cpp $(LAB_DIR)/grade_book.cpp -o $@
$(BUILD)/lab/driver: lab/tests/driver.cpp $(CPP_MODULE) | $(BUILD)/lab
$(CXX) $(CXXFLAGS) -I$(LAB_DIR) lab/tests/driver.cpp $(LAB_DIR)/grade_book.cpp -o $@
$(BUILD) $(BUILD)/fast $(BUILD)/lab:
mkdir -p $@
lab: $(BUILD)/lab/hello $(BUILD)/lab/test_grade_book_c $(BUILD)/lab/driver_c \
$(BUILD)/lab/test_grade_book $(BUILD)/lab/public_port $(BUILD)/lab/driver
# Si existe solution/, test_lab.py también comprueba la solución del laboratorio.
test: all
LAB_DIR=solution python3 -m unittest discover -p 'test_*.py'
test-lab: all
LAB_DIR=$(LAB_DIR) python3 -m unittest -v test_lab
compare:
@echo "módulo en C:"
@wc -l lab/c/grade_book.h lab/c/grade_book.c
@echo "clase en C++ ($(LAB_DIR)):"
@wc -l $(LAB_DIR)/grade_book.h $(LAB_DIR)/grade_book.cpp
@echo "llamadas a malloc, realloc y free en C:"
@cat lab/c/grade_book.c | sed 's://.*::' | grep -E -c '\b(malloc|realloc|free)\(' || true
@echo "llamadas a malloc, realloc y free en C++:"
@cat $(LAB_DIR)/grade_book.cpp | sed 's://.*::' | grep -E -c '\b(malloc|realloc|free)\(' || true
clean:
rm -rf $(BUILD) __pycache__