Skip to article frontmatterSkip to article content

Ejercicios: Análisis de Código

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.