Acerca de¶
A diferencia de otros ejercicios, el objetivo aquí no es escribir código, sino analizarlo. La capacidad de leer, entender y razonar sobre un código existente es una habilidad tan importante como la de escribirlo. Estos ejercicios están diseñados para aplicar los conceptos teóricos de los apuntes sobre Roles de variables y Estado de un programa.
1: Identificación de Roles de Variables¶
1.1: Análisis de una Función¶
Solution to Exercise 1
suma: Acumulador. Su propósito es acumular la suma de los valores positivos encontrados.contador: Contador. Su rol es contar cuántos números positivos se han encontrado.exito: Bandera (Flag). Se utiliza para señalizar al código que llama a la función si la operación fue exitosa (es decir, si se encontró al menos un número positivo).i: Variable de Control de Bucle (Iterador). Su único propósito es controlar las iteraciones del buclefor.Valor de retorno: Variable de Salida. Contiene el resultado principal del cálculo de la función.
1.2: Análisis de Función de Búsqueda¶
Solution to Exercise 2
maximo: Variable de Mejor Candidato/Guardián. Mantiene el valor máximo encontrado hasta el momento.hay_elementos: Bandera de Inicialización. Indica si ya se procesó al menos un elemento para inicializar correctamente la comparación.encontrado: Parámetro de Salida/Bandera de Estado. Comunica al llamador si la operación fue exitosa.indice: Variable de Control de Bucle/Iterador. Controla el recorrido del arreglo.
1.3: Análisis de Función con Múltiples Roles¶
Solution to Exercise 3
resultado: Variable de Salida Estructurada. Encapsula múltiples valores de retorno en una estructura.acumulado: Acumulador. Suma todas las ventas válidas encontradas.dias_activos: Contador. Cuenta los días que tuvieron ventas positivas.primera_venta: Bandera de Primera Vez. Controla la inicialización correcta del máximo.venta_maxima: Variable de Mejor Candidato/Guardián. Mantiene el valor de venta más alto encontrado.dia: Variable de Control de Bucle/Iterador. Controla la iteración a través de los días.
2: Descripción del Estado de un Programa¶
2.1: Fotografía de la Memoria¶
Solution to Exercise 4
Pila (Stack):
Marco de
main:saludo_original(puntero): Contiene la dirección de memoria de la cadena literal “Hola”.saludo_copiado(puntero): Su valor esNULLen este punto, ya que la asignación se produce después de quecrear_copiaretorne.
Marco de
crear_copia:original(puntero): Contiene una copia de la dirección desaludo_original, por lo que también apunta a la cadena literal “Hola”.largo(entero): Su valor es4(calculado por el buclewhile).copia(puntero): Contiene la dirección de memoria del nuevo bloque de 5 bytes reservado pormallocen el montículo.i(entero): Aún no ha sido inicializada, por lo que su valor es indeterminado (basura).
Montículo (Heap):
Hay un bloque de 5 bytes reservado. Su contenido es indeterminado (basura), ya que el bucle
forque lo llena aún no se ha ejecutado.
Segmento de Datos (Solo Lectura):
Contiene la cadena literal
"Hola"(terminada en nulo), que ocupa 5 bytes. La dirección de su primer carácter es a la que apuntansaludo_originalyoriginal.
2.2: Análisis de Memoria con Estructuras¶
Solution to Exercise 5
Pila (Stack):
Marco de
main:emp1(puntero):NULL(aún no asignado, la función no ha retornado)emp2(puntero):NULL
Marco de
crear_empleado:nom(puntero): Apunta a la cadena literal “Ana García” en el segmento de datosedad_emp(int): 28sal(double): 45000.0nuevo(puntero): Apunta al bloque desizeof(empleado_t)bytes en el heaplen_nombre(int): 10 (longitud de “Ana García”)
Montículo (Heap):
Bloque 1:
sizeof(empleado_t)bytes para la estructura empleadonombre(puntero): Apunta al segundo bloque en el heapedad(int): 28salario(double): 45000.0
Bloque 2: 11 bytes conteniendo “Ana García\0”
Segmento de Datos (Solo Lectura):
Cadena literal “Ana García” (11 bytes incluyendo ‘\0’)
2.3: Trazado de Ejecución con Arrays Dinámicos¶
Solution to Exercise 6
PUNTO A (después del malloc en duplicar_array):
Pila:
Marco de
main:numeros[3]: {5, 10, 15}duplicados: NULL
Marco de
duplicar_array:original: apunta anumeros[0]en maintam: 3nuevo: apunta al bloque malloc’eado en heapi: no inicializada (basura)
Heap: Bloque de 12 bytes (3 * sizeof(int)) con contenido indeterminado
PUNTO B (después del bucle for):
Pila: Igual que punto A, excepto
i= 3Heap: El bloque ahora contiene {10, 20, 30}
PUNTO C (antes de llamar a duplicar_array):
Pila:
Marco de
main:numeros[3]: {5, 10, 15}duplicados: NULL
Heap: Vacío
PUNTO D (después de retornar de duplicar_array):
Pila:
Marco de
main:numeros[3]: {5, 10, 15}duplicados: apunta al bloque en heap
Heap: Bloque de 12 bytes conteniendo {10, 20, 30}
3: Análisis de Bugs y Problemas¶
3.1: Identificación de Errores de Lógica¶
Solution to Exercise 7
Problemas identificados:
Falta validación de tamaño: No verifica que
tam >= 2, que es el mínimo necesario para tener un segundo máximo.Elementos duplicados: Si el máximo se repite, el segundo máximo será igual al máximo, lo cual puede no ser el comportamiento deseado.
Array con menos de 2 elementos únicos: Si todos los elementos son iguales, no hay un verdadero “segundo máximo”.
No maneja el caso de array vacío: Si
tam == 0, el comportamiento es indefinido.
Casos que fallan:
Array vacío:
[]Array con un elemento:
[5]Array con elementos iguales:
[3, 3, 3]Array con solo dos valores únicos donde uno se repite:
[5, 3, 5, 5]
Corrección sugerida:
int segundo_maximo_corregido(int arr[], int tam) {
if (tam < 2) {
return INT_MIN; // o manejar error apropiadamente
}
int maximo = INT_MIN;
int segundo = INT_MIN;
for (int i = 0; i < tam; i++) {
if (arr[i] > maximo) {
segundo = maximo;
maximo = arr[i];
} else if (arr[i] > segundo && arr[i] != maximo) {
segundo = arr[i];
}
}
// Verificar si realmente hay un segundo máximo
if (segundo == INT_MIN) {
// Todos los elementos eran iguales
return INT_MIN; // o manejar apropiadamente
}
return segundo;
}3.2: Análisis de Memory Leaks¶
Solution to Exercise 8
Problemas identificados:
Memory leak de
temp: La variabletempse aloca conmalloc()pero nunca se libera confree(). Esto ocurre en ambos caminos de ejecución.Memory leak de
resultado: Cuandolen > 10, se alocaresultadopero luego se alocaresultado_largoy se retorna este último, perdiendo la referencia aresultadosin liberarlo.Memory leaks en
main: Las variablestexto1ytexto2reciben memoria alocada dinámicamente pero nunca se libera.Uso innecesario de memoria: El
tempes redundante ya que se puede trabajar directamente conentrada.
Código corregido:
char* procesar_texto_corregido(const char* entrada) {
int len = strlen(entrada);
char* resultado;
if (len > 10) {
resultado = malloc(len * 3 + 1);
for (int i = 0; i < len; i++) {
resultado[i * 3] = entrada[i];
resultado[i * 3 + 1] = entrada[i];
resultado[i * 3 + 2] = entrada[i];
}
resultado[len * 3] = '\0';
} else {
resultado = malloc(len * 2 + 1);
for (int i = 0; i < len; i++) {
resultado[i * 2] = entrada[i];
resultado[i * 2 + 1] = entrada[i];
}
resultado[len * 2] = '\0';
}
return resultado;
}
int main() {
char* texto1 = procesar_texto_corregido("Hola");
char* texto2 = procesar_texto_corregido("Este es un texto muy largo");
printf("Texto 1: %s\n", texto1);
printf("Texto 2: %s\n", texto2);
// Liberar memoria
free(texto1);
free(texto2);
return 0;
}4: Análisis de Eficiencia y Optimización¶
4.1: Análisis de Complejidad Temporal¶
Solution to Exercise 9
Análisis de buscar_par_suma:
Complejidad temporal: - hay dos bucles anidados que recorren el array
Variables de control:
i: Iterador externo (rol: control de bucle principal)j: Iterador interno (rol: control de bucle secundario, siempre j > i)
Complejidad espacial: - solo usa variables locales
Análisis de buscar_par_suma_optimizado:
Complejidad temporal: - cada elemento se visita máximo una vez
Variables de control:
izq: Puntero izquierdo (rol: guardián del límite inferior)der: Puntero derecho (rol: guardián del límite superior)suma_actual: Variable temporal (rol: almacén de cálculo intermedio)
Complejidad espacial:
Escenarios apropiados:
Primer algoritmo: Cuando el array NO está ordenado y no podemos/queremos ordenarlo
Segundo algoritmo: Cuando el array YA está ordenado o el costo de ordenarlo es amortizable
Trade-offs:
Si necesitamos ordenar: vs
Si ya está ordenado: vs - clara ventaja para el optimizado
4.2: Análisis de Uso de Memoria¶
Solution to Exercise 10
Versión Recursiva:
Pila: - cada llamada recursiva agrega un marco a la pila
Heap: - no usa memoria dinámica
Variables: Solo el parámetro
nen cada marco (rol: parámetro de entrada)Riesgo: Stack overflow para valores grandes de n
Versión Iterativa:
Pila: - un solo marco de función
Heap: - no usa memoria dinámica
Variables:
resultado: Acumulador para el productoi: Variable de control de bucle
Ventajas: Uso mínimo de memoria, sin riesgo de stack overflow
Versión Memoizada:
Pila: - similar a recursiva en la primera llamada, en llamadas subsecuentes
Heap: - array para almacenar resultados calculados
Variables:
cache: Array estático global (rol: almacén de resultados)cache_size: Contador de tamaño actual (rol: guardián de límites)
Trade-off: Usa más memoria pero acelera dramáticamente cálculos repetidos
Comparación de Trade-offs:
| Aspecto | Recursiva | Iterativa | Memoizada |
|---|---|---|---|
| Tiempo (primera llamada) | |||
| Tiempo (llamadas repetidas) | |||
| Espacio en pila | → | ||
| Espacio en heap | |||
| Legibilidad | Alta | Media | Baja |
| Riesgo de overflow | Alto | Bajo | Medio |
5: Ejercicios de Síntesis¶
5.1: Análisis Integral de Sistema¶
Solution to Exercise 11
1. Identificación de Roles por Función:
inicializar_sistema:
capacidad_inicial: Parámetro de entrada (tamaño inicial)sistema: Variable de salida local (estructura a retornar)
agregar_estudiante:
sistema: Parámetro de entrada/salida (modificado por referencia)nombre,edad,promedio: Parámetros de entradanueva_capacidad: Variable temporal (cálculo de redimensión)nueva_lista: Variable temporal/auxiliar (para verificación de realloc)
buscar_mejor_estudiante:
sistema: Parámetro de entrada (solo lectura)mejor: Variable de mejor candidato/guardiáni: Variable de control de bucle
liberar_sistema:
sistema: Parámetro de entrada (puntero a liberar)
2. Análisis de Memoria:
Estado inicial:
Heap:
sizeof(sistema_estudiantes_t)+capacidad_inicial * sizeof(estudiante_t)Pila: Variables locales de cada función
Durante agregado:
Si
cantidad < capacidad: Solo se modifica contenidoSi
cantidad >= capacidad: Realloc duplica el espacio, posible fragmentación
3. Patrones de Diseño:
Dynamic Array: Lista que crece automáticamente
Handle Pattern: El sistema actúa como handle para la colección
RAII-like: Funciones específicas para inicializar y limpiar
4. Análisis de Robustez - Problemas identificados:
Falta de validaciones:
// En agregar_estudiante - faltan validaciones:
if (sistema == NULL || nombre == NULL) {
return false;
}
if (strlen(nombre) >= 50) { // nombre muy largo
return false;
}
if (edad < 0 || edad > 120) { // edad inválida
return false;
}
if (promedio < 0.0 || promedio > 10.0) { // promedio inválido
return false;
}Problemas de robustez:
No validación de punteros NULL
No verificación de límites de strings
No validación de rangos de datos
Posible integer overflow en
nueva_capacidadNo hay función para verificar estado del sistema
Mejoras sugeridas:
Agregar validaciones de entrada en todas las funciones
Implementar códigos de error más específicos
Agregar función de consulta de estado
Considerar límite máximo de capacidad
Agregar tests unitarios para cada función
Este ejercicio integra todos los conceptos de análisis de código, roles de variables, manejo de memoria y buenas prácticas de programación en C.
11: Análisis Avanzado de Punteros¶
11.1: Punteros y Aliasing¶
Solution to Exercise 12
1. Análisis de Aliasing:
En el Caso 1, p1 y p2 apuntan a la misma dirección de memoria (&x). Esto se llama aliasing y provoca que las modificaciones a través de un puntero afecten al otro.
Secuencia de ejecución en Caso 1:
*p1 = valor; // x = 5
*p2 = valor * 2; // x = 10 (sobrescribe el valor anterior)2. Predicción de Salida:
Caso 1:
p1 apunta a: 10 // p1 y p2 apuntan a x, que vale 10
p2 apunta a: 10
Resultado x: 10Caso 2:
p1 apunta a: 5 // y = 5
p2 apunta a: 10 // z = 10
Resultado y: 5, z: 103. Identificación del Problema:
El problema es que la función asume que p1 y p2 apuntan a ubicaciones diferentes. Cuando apuntan a la misma, la segunda asignación sobrescribe la primera, perdiendo el valor original.
4. Roles de Variables:
p1: Parámetro de salida (output parameter) - modifica el valor apuntadop2: Parámetro de salida (output parameter) - modifica otro valor apuntadovalor: Parámetro de entrada (input parameter) - valor base para los cálculos
5. Mejoras Sugeridas:
Opción 1: Verificar aliasing
void procesar_datos(int *p1, int *p2, int valor) {
if (p1 == p2) {
fprintf(stderr, "Error: p1 y p2 no pueden apuntar a la misma dirección\n");
return;
}
*p1 = valor;
*p2 = valor * 2;
}Opción 2: Usar restrict (C99)
void procesar_datos(int *restrict p1, int *restrict p2, int valor) {
*p1 = valor;
*p2 = valor * 2;
}El keyword restrict promete al compilador que los punteros no tienen aliasing.
Opción 3: Documentar el requisito
// PRECONDICIÓN: p1 y p2 deben apuntar a ubicaciones diferentes
// POSTCONDICIÓN: *p1 = valor, *p2 = valor * 2
void procesar_datos(int *p1, int *p2, int valor);11.2: Punteros Colgantes (Dangling Pointers)¶
Solution to Exercise 13
1. Identificación de Punteros Colgantes:
Línea problemática 1: return saludo; en obtener_saludo()
Línea problemática 3: printf("%d\n", arr3[0]); después de procesar_y_liberar()
2. Clasificación de Errores:
Error 1: Retornar dirección de variable local
char* obtener_saludo(void) {
char saludo[50] = "Hola, mundo!";
return saludo; // ¡ERROR!
}Problema: saludo es un arreglo local en el stack. Cuando la función retorna, el stack frame se destruye y la memoria ya no es válida. El puntero retornado apunta a memoria inválida.
Error 2: No anular puntero después de free
void procesar_y_liberar(int** ptr) {
free(*ptr);
// Debería: *ptr = NULL;
}Problema: Después de free(), el puntero sigue apuntando a la misma dirección, pero esa memoria ya fue liberada. Esto es un dangling pointer.
3. Análisis de Memoria por Escenario:
Escenario 1:
Stack antes de retornar:
[saludo: "Hola, mundo!"] <- puntero s1 apuntará aquí
Stack después de retornar:
[basura o datos de otra función] <- s1 apunta a zona inválidaResultado: Comportamiento indefinido. Podría funcionar “por casualidad” o crashear.
Escenario 2:
Segmento de datos (static):
arr1 -> [1][2][3][4][5] <- Permanece durante toda la ejecuciónResultado: OK. Las variables static persisten.
Escenario 3:
Heap:
arr2 -> [1][2][3][4][5] <- Memoria reservada con malloc
... uso ...
free(arr2) <- Memoria liberada
arr2 sigue apuntando aquí (pero ya no es válido)Resultado: OK porque se usa antes de free.
Escenario 4:
Heap:
arr3 -> [1][2][3][4][5]
free(*ptr) <- Memoria liberada
arr3 -> [???] (memoria inválida) <- Dangling pointer
printf("%d\n", arr3[0]) <- Acceso a memoria liberadaResultado: Comportamiento indefinido. Podría:
Imprimir basura
Causar segmentation fault
“Funcionar” si la memoria no fue reutilizada aún
4. Comportamiento al Ejecutar:
Escenario 1: Probablemente imprime basura o el saludo “por suerte”
Escenario 2: Funciona correctamente
Escenario 3: Funciona correctamente
Escenario 4: Comportamiento indefinido, muy probable crash
5. Correcciones:
Corrección Función A - Opción 1: Usar malloc
char* obtener_saludo(void) {
char* saludo = malloc(50);
if (saludo == NULL) return NULL;
strcpy(saludo, "Hola, mundo!");
return saludo; // OK: memoria en heap persiste
}
// El llamador debe hacer free(saludo)Corrección Función A - Opción 2: Usar static
char* obtener_saludo(void) {
static char saludo[50] = "Hola, mundo!";
return saludo; // OK: variable static persiste
}
// NOTA: No es thread-safeCorrección Función A - Opción 3: Pasar buffer
void obtener_saludo(char* buffer, size_t tam) {
if (buffer == NULL || tam < 14) return;
strcpy(buffer, "Hola, mundo!");
}
// Uso:
char saludo[50];
obtener_saludo(saludo, sizeof(saludo));Corrección Función D:
void procesar_y_liberar(int** ptr) {
if (ptr == NULL || *ptr == NULL) return;
free(*ptr);
*ptr = NULL; // IMPORTANTE: Anular el puntero
}Corrección main():
int* arr3 = crear_arreglo_dinamico();
procesar_y_liberar(&arr3);
// No usar arr3 aquí
if (arr3 != NULL) { // Esto ahora es false
printf("%d\n", arr3[0]);
}Buenas Prácticas:
Siempre anular punteros después de
free()No retornar direcciones de variables locales
Documentar quién es responsable de liberar memoria
Usar herramientas como Valgrind para detectar estos errores
Considerar patrones como RAII (en C con funciones de limpieza)
11.3: Aritmética de Punteros y Límites de Arreglos¶
Solution to Exercise 14
1. Propósito de Funciones:
funcion_a: Duplica todos los elementos del arreglofuncion_b: Imprime todos los elementos del arreglofuncion_c: Copia una cadena (implementación manual destrcpy)funcion_d: Invierte el arreglo in-place
2. Análisis de Aritmética de Punteros:
funcion_a:
int *p = arr; // p apunta al primer elemento
int *fin = arr + n; // fin apunta una posición DESPUÉS del último elemento
while (p < fin) { // Itera mientras p no llegue a fin
*p = *p * 2; // Desreferencia: modifica el elemento actual
p++; // Avanza al siguiente elemento (p += sizeof(int))
}Equivalente con índices:
for (size_t i = 0; i < n; i++) {
arr[i] = arr[i] * 2;
}funcion_b:
for (int *p = arr; p < arr + n; p++) {
// p recorre desde arr[0] hasta arr[n-1]
printf("%d ", *p);
}funcion_c:
while (*origen != '\0') { // Itera hasta encontrar el terminador
*destino = *origen; // Copia carácter por carácter
destino++; // Avanza ambos punteros
origen++;
}
*destino = '\0'; // Agrega terminador al destinofuncion_d:
int *inicio = arr; // Apunta al primer elemento
int *fin = arr + n - 1; // Apunta al último elemento
while (inicio < fin) { // Se encuentran en el medio
// Intercambia *inicio con *fin
int temp = *inicio;
*inicio = *fin;
*fin = temp;
inicio++; // Avanzan hacia el centro
fin--;
}3. Roles de Punteros:
funcion_a:
arr: Parámetro de entrada/salida (array a modificar)p: Iterador/caminante (walker) - recorre el arreglofin: Centinela (sentinel) - marca el límite superior
funcion_b:
arr: Parámetro de entrada (array a imprimir)p: Iterador/caminante - recorre el arreglo
funcion_c:
destino: Parámetro de salida (destino de la copia)origen: Parámetro de entrada (fuente de datos)Ambos actúan como caminantes que avanzan en paralelo
funcion_d:
arr: Parámetro de entrada/salidainicio: Caminante que avanza desde el principiofin: Caminante que retrocede desde el finaltemp: Variable auxiliar temporal para intercambio
4. Predicción de Salida:
Estado inicial: arreglo = {1, 2, 3, 4, 5}
Después de funcion_a: {2, 4, 6, 8, 10}
Salida de funcion_b: "2 4 6 8 10 "
Después de funcion_c: destino = "Hola"
Salida: "Hola"
Después de funcion_d: arreglo = {10, 8, 6, 4, 2}
Salida de funcion_b: "10 8 6 4 2 "5. Problemas de Seguridad:
funcion_a y funcion_b:
Puntero NULL: Si
arr == NULL, desreferencia causa crashn inválido: Si
n = 0, funciona pero no hace nada. Sines muy grande, acceso fuera de límites
Validación sugerida:
void funcion_a(int *arr, size_t n) {
if (arr == NULL || n == 0) return;
// ... resto del código
}funcion_c - Problema crítico:
void funcion_c(char *destino, const char *origen) {
// NO verifica el tamaño de destino
// Puede causar BUFFER OVERFLOW
}Escenario peligroso:
char origen[] = "Esta cadena es muy larga";
char destino[5]; // Solo 5 bytes!
funcion_c(destino, origen); // BUFFER OVERFLOWVersión segura:
void funcion_c_segura(char *destino, const char *origen, size_t tam_destino) {
if (destino == NULL || origen == NULL || tam_destino == 0) return;
while (*origen != '\0' && tam_destino > 1) {
*destino = *origen;
destino++;
origen++;
tam_destino--;
}
*destino = '\0';
}O usar strncpy:
strncpy(destino, origen, sizeof(destino) - 1);
destino[sizeof(destino) - 1] = '\0';funcion_d:
Puntero NULL: Crash si
arr == NULLn = 0:
arr + n - 1apunta ANTES dearr(comportamiento indefinido)n = 1: Funciona correctamente (no hace nada)
Validación sugerida:
void funcion_d(int *arr, size_t n) {
if (arr == NULL || n <= 1) return;
// ... resto del código
}6. Comparación: Punteros vs Índices
Ventajas de punteros:
Más eficiente en algunos compiladores (menos operaciones)
Más “idiomático” en C para ciertos algoritmos
No necesita calcular
arr[i]en cada iteración
Ventajas de índices:
Más legible y fácil de entender
Más seguro (menos probabilidad de errores)
Mejor para debugging (puedes ver el índice)
No pierde la referencia al inicio del arreglo
Comparación de rendimiento:
Compiladores modernos con optimización suelen generar el mismo código máquina para ambas versiones, así que la legibilidad debería ser el factor decisivo.
Recomendación: Usar índices por defecto, punteros solo cuando:
La lógica sea más clara con punteros (ej:
funcion_d)Se necesite pasar punteros intermedios
Se implemente una interfaz que requiera punteros
Ejemplo de refactorización de funcion_a con índices:
void funcion_a(int *arr, size_t n) {
if (arr == NULL) return;
for (size_t i = 0; i < n; i++) {
arr[i] *= 2;
}
}11.4: Punteros a Funciones y Callbacks¶
Solution to Exercise 15
1. Análisis de Tipos:
typedef bool (*predicado_t)(int);
typedef int (*operacion_t)(int);predicado_t:
Es un tipo de puntero a función
La función apuntada debe recibir un
inty retornarboolSe usa para funciones que evalúan condiciones/predicados
operacion_t:
Es un tipo de puntero a función
La función apuntada debe recibir un
inty retornarintSe usa para funciones que transforman valores
Sintaxis explicada:
// Sin typedef (más confuso):
bool (*predicado)(int); // Variable que es puntero a función
// Con typedef (más claro):
predicado_t predicado; // Variable de tipo "puntero a función"2. Roles de Punteros a Función:
En contar_si:
predicado: Callback/Strategy - define la estrategia de filtradoRol: Inyección de dependencia - el comportamiento se define externamente
En aplicar:
operacion: Callback/Transformer - define cómo transformar cada elementoRol: Separación de iteración y operación
En procesar:
filtro: Callback/Predicate - decide qué elementos procesartransformacion: Callback/Transformer - define cómo procesarRol: Composición de comportamientos
3. Flujo de Ejecución:
Llamada: contar_si(numeros, n, es_par)
Paso 1: Entrada a contar_si
arr = &numeros[0]
n = 9
predicado = dirección de es_par
Paso 2: Inicialización
contador = 0
Paso 3: Iteración i=0
arr[0] = -3
predicado(-3) -> llamada a es_par(-3)
return -3 % 2 == 0 -> false
if (false) -> no incrementa contador
Paso 4: Iteración i=1
arr[1] = -2
predicado(-2) -> llamada a es_par(-2)
return -2 % 2 == 0 -> true
if (true) -> contador++ (contador = 1)
Paso 5: Iteración i=2
arr[2] = -1
predicado(-1) -> es_par(-1) -> false
No incrementa
Paso 6: Iteración i=3
arr[3] = 0
predicado(0) -> es_par(0) -> true
contador++ (contador = 2)
... continúa ...
Resultado: contador = 5 (elementos: -2, 0, 2, 4)4. Predicción de Resultados:
copia1 después de aplicar(copia1, 5, duplicar):
Estado inicial: {1, 2, 3, 4, 5}
Después de aplicar duplicar a cada elemento:
copia1 = {2, 4, 6, 8, 10}copia2 después de procesar(copia2, 5, es_positivo, cuadrado):
Estado inicial: {-2, -1, 0, 1, 2}
Procesamiento:
- copia2[0] = -2 -> es_positivo(-2) = false -> no transforma
- copia2[1] = -1 -> es_positivo(-1) = false -> no transforma
- copia2[2] = 0 -> es_positivo(0) = false -> no transforma
- copia2[3] = 1 -> es_positivo(1) = true -> cuadrado(1) = 1
- copia2[4] = 2 -> es_positivo(2) = true -> cuadrado(2) = 4
Estado final: {-2, -1, 0, 1, 4}Salida completa del programa:
Pares: 5
Positivos: 55. Ventajas del Patrón:
Sin callbacks (enfoque ingenuo):
int contar_pares(const int *arr, size_t n) {
int contador = 0;
for (size_t i = 0; i < n; i++) {
if (arr[i] % 2 == 0) contador++;
}
return contador;
}
int contar_positivos(const int *arr, size_t n) {
int contador = 0;
for (size_t i = 0; i < n; i++) {
if (arr[i] > 0) contador++;
}
return contador;
}
int contar_negativos(const int *arr, size_t n) {
// Otra función casi idéntica...
}
// ¡Mucha duplicación!Con callbacks (enfoque genérico):
// Una sola función, infinitas posibilidades
int contar_si(const int *arr, size_t n, predicado_t pred);
// Uso:
contar_si(arr, n, es_par);
contar_si(arr, n, es_positivo);
contar_si(arr, n, es_negativo);
contar_si(arr, n, es_multiplo_de_tres);
// ... cualquier predicadoVentajas:
DRY (Don’t Repeat Yourself): Evita duplicación de código
Extensibilidad: Agregar nuevas condiciones sin modificar código existente
Composición: Combinar comportamientos (como en
procesar)Testabilidad: Más fácil probar funciones pequeñas e independientes
Reusabilidad: Las funciones de alto nivel sirven para múltiples propósitos
Patrón de diseño: Strategy Pattern - el comportamiento se selecciona en tiempo de ejecución
6. Extensión - Encontrar Primer Elemento:
int* encontrar_si(int *arr, size_t n, predicado_t predicado) {
for (size_t i = 0; i < n; i++) {
if (predicado(arr[i])) {
return &arr[i]; // Retorna puntero al primer elemento que cumple
}
}
return NULL; // No se encontró
}
// Uso:
int numeros[] = {-3, -2, -1, 0, 1, 2, 3};
int *primer_positivo = encontrar_si(numeros, 7, es_positivo);
if (primer_positivo != NULL) {
printf("Primer positivo: %d\n", *primer_positivo);
} else {
printf("No hay positivos\n");
}Extensión adicional - Con contexto:
A veces los callbacks necesitan estado adicional. Se puede extender usando un puntero void*:
typedef bool (*predicado_ctx_t)(int, void*);
int contar_si_ctx(const int *arr, size_t n,
predicado_ctx_t pred, void* contexto) {
int contador = 0;
for (size_t i = 0; i < n; i++) {
if (pred(arr[i], contexto)) {
contador++;
}
}
return contador;
}
// Ejemplo: contar mayores que un umbral
bool es_mayor_que(int valor, void* ctx) {
int umbral = *(int*)ctx;
return valor > umbral;
}
// Uso:
int umbral = 3;
int cantidad = contar_si_ctx(numeros, n, es_mayor_que, &umbral);Comparación con otros lenguajes:
Esta técnica en C es la base de conceptos más avanzados en otros lenguajes:
C++: Templates, functors, lambdas
Python: Lambdas, funciones de orden superior
JavaScript: Callbacks,
map,filter,reduce
Ejemplo equivalente en Python:
numeros = [-3, -2, -1, 0, 1, 2, 3, 4, 5]
pares = len(list(filter(lambda x: x % 2 == 0, numeros)))
positivos = len(list(filter(lambda x: x > 0, numeros)))11.5: Análisis Integral - Sistema de Gestión con Punteros¶
Solution to Exercise 16
1. Análisis Completo de Punteros:
Tipos de punteros identificados:
a) Puntero a estructura (lista_t*):
Maneja el TAD lista como handle opaco
Permite modificar la estructura desde funciones
b) Puntero a nodo (nodo_t*):
Enlaces en la lista enlazada
Variables temporales para recorrido
c) Puntero a dato (int*):
Parámetro de salida en
lista_eliminar_primeroParámetro de entrada/salida en callbacks
d) Puntero a función:
void (*func)(int*, void*)enlista_aplicarbool (*pred)(int, void*)enlista_filtrar
e) Puntero genérico (void*):
Contexto adicional para callbacks
Permite pasar datos de cualquier tipo
2. Diagrama de Memoria (después de 3 inserciones):
Stack (main):
┌────────────────┐
│ lista ───────┼─────┐
└────────────────┘ │
│
Heap: ↓
┌─────────────────────────┐
│ lista_t │
│ ┌─────────┬───────────┐│
│ │primero─┼─┐ ││
│ │ultimo──┼─┼───┐ ││
│ │tamanio:3│ │ │ ││
│ └─────────┴─┼───┼─────┘│
└─────────────┼───┼───────┘
│ │
↓ │
┌─────────────────┐
│ nodo_t (dato=1) │
│ ┌────┬────────┐ │
│ │ 1 │siguiente┼─┐
│ └────┴────────┘ │ │
└─────────────────┘ │
↓
┌─────────────────┐
│ nodo_t (dato=2) │
│ ┌────┬────────┐ │
│ │ 2 │siguiente┼─┐
│ └────┴────────┘ │ │
└─────────────────┘ │
↓
┌─────────────────┐
│ nodo_t (dato=3) │
│ ┌────┬────────┐ │
│ │ 3 │ NULL │ │◄─┐
│ └────┴────────┘ │ │
└─────────────────┘ │
│
ultimo apunta aquí ──────────────────┘3. Roles de Variables por Función:
lista_crear:
lista: Variable local de salida - estructura recién creada
lista_insertar_final:
lista: Parámetro de entrada/salida - se modificadato: Parámetro de entrada - valor a insertarnuevo: Variable temporal - nodo recién creado
lista_eliminar_primero:
lista: Parámetro de entrada/salida - se modificadato: Parámetro de salida - valor extraídotemp: Variable temporal - guarda referencia para free
lista_destruir:
lista: Parámetro de entrada - estructura a liberaractual: Caminante/iterador - recorre la listasiguiente: Variable temporal - guarda siguiente antes de liberar actual
lista_aplicar:
lista: Parámetro de entrada/salida - se pueden modificar elementosfunc: Callback - operación a aplicarextra: Contexto - datos adicionales para callbackactual: Caminante - recorre la lista
lista_filtrar:
lista: Parámetro de entrada - lista original (const, no modificable)pred: Callback - condición de filtradoextra: Contexto - datos para predicadonueva: Variable local de salida - lista filtradaactual: Caminante - recorre lista original
4. Invariantes de la Estructura:
Invariantes mantenidos:
Si
primero == NULL, entoncesultimo == NULL(lista vacía)Si
primero != NULL, entoncesultimo != NULL(lista no vacía)ultimo->siguiente == NULL(último nodo apunta a NULL)tamaniorefleja la cantidad real de nodosSi
tamanio == 0, entoncesprimero == NULLSi
tamanio == 1, entoncesprimero == ultimoTodos los nodos son alcanzables desde
primerosiguiendo los enlaces
Verificación en operaciones:
Insertar en lista vacía:
if (lista->primero == NULL) {
lista->primero = nuevo;
lista->ultimo = nuevo; // Mantiene invariante 1
}Eliminar último elemento:
lista->primero = temp->siguiente;
if (lista->primero == NULL) {
lista->ultimo = NULL; // Mantiene invariante 1
}5. Gestión de Memoria - Ciclo de Vida:
Fase 1: Creación
malloc(lista_t) → Heap block 1Fase 2: Inserciones (i=1 a 5)
malloc(nodo_t) → Heap block 2 (dato=1)
malloc(nodo_t) → Heap block 3 (dato=2)
malloc(nodo_t) → Heap block 4 (dato=3)
malloc(nodo_t) → Heap block 5 (dato=4)
malloc(nodo_t) → Heap block 6 (dato=5)Fase 3: Aplicar duplicar
No hay nuevas allocations
Se modifican datos in-placeFase 4: Filtrar
malloc(lista_t) → Heap block 7 (nueva lista)
malloc(nodo_t) → Heap block 8 (dato=6)
malloc(nodo_t) → Heap block 9 (dato=8)
malloc(nodo_t) → Heap block 10 (dato=10)Fase 5: Destrucción
free(block 2), free(block 3), ..., free(block 6)
free(block 1)
free(block 8), free(block 9), free(block 10)
free(block 7)Total memoria usada: ~10-12 bloques heap en el pico de ejecución
6. Análisis de Robustez:
Validaciones existentes:
✅ Verificación de NULL en lista_crear después de malloc
✅ Verificación de NULL en lista_insertar_final
✅ Verificación de lista vacía en lista_eliminar_primero
✅ Verificación de NULL en lista_destruir
✅ Verificación de parámetros en lista_aplicar y lista_filtrar
Problemas potenciales identificados:
❌ lista_crear no verifica malloc de lista:
lista_t* lista = malloc(sizeof(lista_t));
if (lista == NULL) return NULL; // ✅ PERO...
// No inicializa campos si malloc falla parcialmente❌ lista_insertar_final no verifica malloc de nuevo nodo:
Sí lo hace, está correcto.
❌ Falta de manejo de errores propagado:
if (!lista_insertar_final(nueva, actual->dato)) {
lista_destruir(nueva); // ✅ Buena práctica
return NULL;
}❌ lista_aplicar no maneja excepciones en callback:
Si el callback falla (ej. acceso inválido), no hay forma de recuperarse.
Mejoras sugeridas:
// 1. Agregar función de validación
bool lista_es_valida(const lista_t* lista) {
if (lista == NULL) return false;
// Verificar invariantes
if (lista->primero == NULL && lista->ultimo != NULL) return false;
if (lista->primero != NULL && lista->ultimo == NULL) return false;
// Contar nodos
size_t contador = 0;
nodo_t* actual = lista->primero;
while (actual != NULL && contador <= lista->tamanio) {
contador++;
actual = actual->siguiente;
}
return contador == lista->tamanio;
}
// 2. Agregar códigos de error más específicos
typedef enum {
LISTA_OK = 0,
LISTA_ERROR_MEMORIA,
LISTA_ERROR_PARAMETRO_INVALIDO,
LISTA_ERROR_LISTA_VACIA
} lista_error_t;
// 3. Versión robusta de insertar
lista_error_t lista_insertar_final_v2(lista_t* lista, int dato) {
if (lista == NULL) return LISTA_ERROR_PARAMETRO_INVALIDO;
nodo_t* nuevo = malloc(sizeof(nodo_t));
if (nuevo == NULL) return LISTA_ERROR_MEMORIA;
// ... resto de la lógica
return LISTA_OK;
}7. Complejidad Temporal y Espacial:
lista_crear:
Temporal:
Espacial: - un bloque de tamaño fijo
lista_insertar_final:
Temporal: - acceso directo al último
Espacial: - un nodo nuevo
lista_eliminar_primero:
Temporal: - acceso directo al primero
Espacial: - libera un nodo
lista_destruir:
Temporal: - recorre todos los nodos
Espacial: - usa espacio constante auxiliar
lista_aplicar:
Temporal: - aplica callback a cada elemento
Espacial: - sin memoria adicional (modificación in-place)
lista_filtrar:
Temporal: - recorre y copia elementos filtrados
Espacial: - nueva lista con k elementos (k ≤ n)
Comparación con otras implementaciones:
| Operación | Lista Enlazada | Arreglo Dinámico |
|---|---|---|
| Insertar al final | con tail | amortizado |
| Eliminar primero | (desplazar) | |
| Acceso por índice | ||
| Memoria overhead | Alta (punteros) | Baja + desperdicio |
Conclusión: Esta implementación es óptima para operaciones en los extremos, pero ineficiente para acceso aleatorio.
Estos 5 ejercicios cubren conceptos avanzados de punteros incluyendo aliasing, punteros colgantes, aritmética de punteros, callbacks con punteros a funciones, y un análisis integral de un sistema completo con múltiples tipos de punteros.