Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Ejercicios: Análisis de Código

Universidad Nacional de Rio Negro - Sede Andina

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
  1. suma: Acumulador. Su propósito es acumular la suma de los valores positivos encontrados.

  2. contador: Contador. Su rol es contar cuántos números positivos se han encontrado.

  3. 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).

  4. i: Variable de Control de Bucle (Iterador). Su único propósito es controlar las iteraciones del bucle for.

  5. 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
  1. maximo: Variable de Mejor Candidato/Guardián. Mantiene el valor máximo encontrado hasta el momento.

  2. hay_elementos: Bandera de Inicialización. Indica si ya se procesó al menos un elemento para inicializar correctamente la comparación.

  3. encontrado: Parámetro de Salida/Bandera de Estado. Comunica al llamador si la operación fue exitosa.

  4. 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
  1. resultado: Variable de Salida Estructurada. Encapsula múltiples valores de retorno en una estructura.

  2. acumulado: Acumulador. Suma todas las ventas válidas encontradas.

  3. dias_activos: Contador. Cuenta los días que tuvieron ventas positivas.

  4. primera_venta: Bandera de Primera Vez. Controla la inicialización correcta del máximo.

  5. venta_maxima: Variable de Mejor Candidato/Guardián. Mantiene el valor de venta más alto encontrado.

  6. 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 es NULL en este punto, ya que la asignación se produce después de que crear_copia retorne.

    • Marco de crear_copia:

      • original (puntero): Contiene una copia de la dirección de saludo_original, por lo que también apunta a la cadena literal “Hola”.

      • largo (entero): Su valor es 4 (calculado por el bucle while).

      • copia (puntero): Contiene la dirección de memoria del nuevo bloque de 5 bytes reservado por malloc en 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 for que 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 apuntan saludo_original y original.

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 datos

      • edad_emp (int): 28

      • sal (double): 45000.0

      • nuevo (puntero): Apunta al bloque de sizeof(empleado_t) bytes en el heap

      • len_nombre (int): 10 (longitud de “Ana García”)

  • Montículo (Heap):

    • Bloque 1: sizeof(empleado_t) bytes para la estructura empleado

      • nombre (puntero): Apunta al segundo bloque en el heap

      • edad (int): 28

      • salario (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 a numeros[0] en main

      • tam: 3

      • nuevo: apunta al bloque malloc’eado en heap

      • i: 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 = 3

  • Heap: 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:

  1. Falta validación de tamaño: No verifica que tam >= 2, que es el mínimo necesario para tener un segundo máximo.

  2. 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.

  3. Array con menos de 2 elementos únicos: Si todos los elementos son iguales, no hay un verdadero “segundo máximo”.

  4. 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:

  1. Memory leak de temp: La variable temp se aloca con malloc() pero nunca se libera con free(). Esto ocurre en ambos caminos de ejecución.

  2. Memory leak de resultado: Cuando len > 10, se aloca resultado pero luego se aloca resultado_largo y se retorna este último, perdiendo la referencia a resultado sin liberarlo.

  3. Memory leaks en main: Las variables texto1 y texto2 reciben memoria alocada dinámicamente pero nunca se libera.

  4. Uso innecesario de memoria: El temp es redundante ya que se puede trabajar directamente con entrada.

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: O(n2)O(n^2) - 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: O(1)O(1) - solo usa variables locales

Análisis de buscar_par_suma_optimizado:

  • Complejidad temporal: O(n)O(n) - 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: O(1)O(1)

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: O(nlogn)+O(n)O(n log n) + O(n) vs O(n2)O(n^2)

  • Si ya está ordenado: O(n)O(n) vs O(n2)O(n^2) - clara ventaja para el optimizado

4.2: Análisis de Uso de Memoria

Solution to Exercise 10

Versión Recursiva:

  • Pila: O(n)O(n) - cada llamada recursiva agrega un marco a la pila

  • Heap: O(1)O(1) - no usa memoria dinámica

  • Variables: Solo el parámetro n en cada marco (rol: parámetro de entrada)

  • Riesgo: Stack overflow para valores grandes de n

Versión Iterativa:

  • Pila: O(1)O(1) - un solo marco de función

  • Heap: O(1)O(1) - no usa memoria dinámica

  • Variables:

    • resultado: Acumulador para el producto

    • i: Variable de control de bucle

  • Ventajas: Uso mínimo de memoria, sin riesgo de stack overflow

Versión Memoizada:

  • Pila: O(n)O(n) - similar a recursiva en la primera llamada, O(1)O(1) en llamadas subsecuentes

  • Heap: O(n)O(n) - 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:

AspectoRecursivaIterativaMemoizada
Tiempo (primera llamada)O(n)O(n)O(n)O(n)O(n)O(n)
Tiempo (llamadas repetidas)O(n)O(n)O(n)O(n)O(1)O(1)
Espacio en pilaO(n)O(n)O(1)O(1)O(n)O(n)O(1)O(1)
Espacio en heapO(1)O(1)O(1)O(1)O(n)O(n)
LegibilidadAltaMediaBaja
Riesgo de overflowAltoBajoMedio

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 entrada

  • nueva_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án

  • i: 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 contenido

  • Si 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_capacidad

  • No 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: 10

Caso 2:

p1 apunta a: 5     // y = 5
p2 apunta a: 10    // z = 10
Resultado y: 5, z: 10

3. 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 apuntado

  • p2: Parámetro de salida (output parameter) - modifica otro valor apuntado

  • valor: 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álida

Resultado: 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ón

Resultado: 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 liberada

Resultado: 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-safe

Correcció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:

  1. Siempre anular punteros después de free()

  2. No retornar direcciones de variables locales

  3. Documentar quién es responsable de liberar memoria

  4. Usar herramientas como Valgrind para detectar estos errores

  5. 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 arreglo

  • funcion_b: Imprime todos los elementos del arreglo

  • funcion_c: Copia una cadena (implementación manual de strcpy)

  • 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 destino

funcion_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 arreglo

  • fin: 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/salida

  • inicio: Caminante que avanza desde el principio

  • fin: Caminante que retrocede desde el final

  • temp: 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 crash

  • n inválido: Si n = 0, funciona pero no hace nada. Si n es 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 OVERFLOW

Versió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 == NULL

  • n = 0: arr + n - 1 apunta ANTES de arr (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 int y retornar bool

  • Se 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 int y retornar int

  • Se 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 filtrado

  • Rol: Inyección de dependencia - el comportamiento se define externamente

En aplicar:

  • operacion: Callback/Transformer - define cómo transformar cada elemento

  • Rol: Separación de iteración y operación

En procesar:

  • filtro: Callback/Predicate - decide qué elementos procesar

  • transformacion: Callback/Transformer - define cómo procesar

  • Rol: 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: 5

5. 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 predicado

Ventajas:

  1. DRY (Don’t Repeat Yourself): Evita duplicación de código

  2. Extensibilidad: Agregar nuevas condiciones sin modificar código existente

  3. Composición: Combinar comportamientos (como en procesar)

  4. Testabilidad: Más fácil probar funciones pequeñas e independientes

  5. 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_primero

  • Parámetro de entrada/salida en callbacks

d) Puntero a función:

  • void (*func)(int*, void*) en lista_aplicar

  • bool (*pred)(int, void*) en lista_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 modifica

  • dato: Parámetro de entrada - valor a insertar

  • nuevo: Variable temporal - nodo recién creado

lista_eliminar_primero:

  • lista: Parámetro de entrada/salida - se modifica

  • dato: Parámetro de salida - valor extraído

  • temp: Variable temporal - guarda referencia para free

lista_destruir:

  • lista: Parámetro de entrada - estructura a liberar

  • actual: Caminante/iterador - recorre la lista

  • siguiente: Variable temporal - guarda siguiente antes de liberar actual

lista_aplicar:

  • lista: Parámetro de entrada/salida - se pueden modificar elementos

  • func: Callback - operación a aplicar

  • extra: Contexto - datos adicionales para callback

  • actual: Caminante - recorre la lista

lista_filtrar:

  • lista: Parámetro de entrada - lista original (const, no modificable)

  • pred: Callback - condición de filtrado

  • extra: Contexto - datos para predicado

  • nueva: Variable local de salida - lista filtrada

  • actual: Caminante - recorre lista original

4. Invariantes de la Estructura:

Invariantes mantenidos:

  1. Si primero == NULL, entonces ultimo == NULL (lista vacía)

  2. Si primero != NULL, entonces ultimo != NULL (lista no vacía)

  3. ultimo->siguiente == NULL (último nodo apunta a NULL)

  4. tamanio refleja la cantidad real de nodos

  5. Si tamanio == 0, entonces primero == NULL

  6. Si tamanio == 1, entonces primero == ultimo

  7. Todos los nodos son alcanzables desde primero siguiendo 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 1

Fase 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-place

Fase 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: O(1)O(1)

  • Espacial: O(1)O(1) - un bloque de tamaño fijo

lista_insertar_final:

  • Temporal: O(1)O(1) - acceso directo al último

  • Espacial: O(1)O(1) - un nodo nuevo

lista_eliminar_primero:

  • Temporal: O(1)O(1) - acceso directo al primero

  • Espacial: O(1)O(1) - libera un nodo

lista_destruir:

  • Temporal: O(n)O(n) - recorre todos los nodos

  • Espacial: O(1)O(1) - usa espacio constante auxiliar

lista_aplicar:

  • Temporal: O(nTcallback)O(n \cdot T_{callback}) - aplica callback a cada elemento

  • Espacial: O(1)O(1) - sin memoria adicional (modificación in-place)

lista_filtrar:

  • Temporal: O(n(Tpred+Tinsertar))=O(n)O(n \cdot (T_{pred} + T_{insertar})) = O(n) - recorre y copia elementos filtrados

  • Espacial: O(k)O(k) - nueva lista con k elementos (k ≤ n)

Comparación con otras implementaciones:

OperaciónLista EnlazadaArreglo Dinámico
Insertar al finalO(1)O(1) con tailO(1)O(1) amortizado
Eliminar primeroO(1)O(1)O(n)O(n) (desplazar)
Acceso por índiceO(n)O(n)O(1)O(1)
Memoria overheadAlta (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.