Skip to content

Solutions: Lecture 6 In-Class Exercises

Solutions to the Lecture 6 in-class exercises. Try each exercise yourself before reading these.


Part A - Stacks and lifetimes (pen and paper)

Exercise A1 - Trace the frames

int sum_to(int n) {
    if (n == 0) {
        return 0;
    }
    return n + sum_to(n - 1);
}
  1. At the deepest point, four frames are on the stack:
   top ->  sum_to(n = 0)   returns 0
           sum_to(n = 1)   returns 1 + 0 = 1
           sum_to(n = 2)   returns 2 + 1 = 3
   bottom  sum_to(n = 3)   returns 3 + 3 = 6
  1. They return top to bottom (innermost first): sum_to(0) returns 0, then sum_to(1) returns 1, then sum_to(2) returns 3, then sum_to(3) returns 6. The frame that was pushed last is popped first.
  2. Each call gets its own frame with its own n. The four n values (3, 2, 1, 0) live in four separate frames on the stack at once, so they never overwrite one another - that is exactly what lets recursion work.

Exercise A2 - Spot the bug

  • (a) Returning the address of a local. x lives in first's stack frame, which is popped when first returns, so &x dangles. Fix: return the value (int first(void) { return 10; }), or allocate x on the heap and return that pointer (caller frees).
  • (b) Use-after-free (dangling pointer). After free(p), the block is no longer yours; *p reads freed memory. Fix: print before free(p), or do not free until you are truly done. Setting p = NULL; after the free turns a later stray use into a loud crash.
  • (c) Memory leak. a is allocated but never freed; when load returns, the pointer is gone and the block can never be reclaimed. Fix: free(a); before the function returns.

Part B - The heap at the keyboard

Exercise B1 - A dynamic array

#include <stdio.h>
#include <stdlib.h>

int main(void) {
    int n;
    printf("How many numbers? ");
    scanf("%d", &n);

    int *a = malloc(n * sizeof(int));
    if (a == NULL) {
        return 1;
    }

    printf("Enter %d numbers: ", n);
    for (int i = 0; i < n; i++) {
        scanf("%d", &a[i]);
    }

    int largest = a[0];
    for (int i = 1; i < n; i++) {
        if (a[i] > largest) {
            largest = a[i];
        }
    }
    printf("largest: %d\n", largest);

    free(a);
    return 0;
}
How many numbers? 4
Enter 4 numbers: 10 25 30 7
largest: 30
  • malloc(n * sizeof(int)) reserves exactly n ints - a size known only at run time, which a stack array could not do. The NULL check guards against a failed allocation before any a[i] runs.
  • a[i] works on the heap block exactly as on a stack array, because a[i] is *(a + i) for any pointer. Seed largest from a[0], then scan from i = 1.
  • One malloc, one free. Nothing reclaims the block for you.
  • Stretch (average): double avg = (double) sum / n; after summing in the same loop; cast before dividing to avoid integer division.

Exercise B2 - Return a heap array

#include <stdio.h>
#include <stdlib.h>

int *make_squares(int n) {
    int *a = malloc(n * sizeof(int));
    if (a == NULL) {
        return NULL;
    }
    for (int i = 0; i < n; i++) {
        a[i] = i * i;
    }
    return a;
}

int main(void) {
    int *sq = make_squares(5);
    if (sq == NULL) {
        return 1;
    }
    for (int i = 0; i < 5; i++) {
        printf("%d ", sq[i]);
    }
    printf("\n");

    free(sq);          /* main owns the block now */
    return 0;
}
0 1 4 9 16
  • make_squares allocates on the heap, so the block survives after the function returns and its frame is popped. The local pointer a disappears on return, but the block it named does not.
  • Ownership passes to the caller. main receives the pointer and is responsible for the single free. The function does not free what it returns - that would hand back a dangling pointer.
  • Stretch (the broken sibling):
int *make_squares_bad(int n) {
    int a[100];
    for (int i = 0; i < n; i++) {
        a[i] = i * i;
    }
    return a;          /* WARNING: address of a local; dangles on return */
}

a lives in the function's frame, popped on return, so the returned address is dead - the compiler warns about exactly this. B2's heap block has no such problem because its lifetime is manual, not tied to the frame.


Stretch

Concatenate two arrays onto the heap

#include <stdio.h>
#include <stdlib.h>

int *concat(const int *a, int na, const int *b, int nb) {
    int *out = malloc((na + nb) * sizeof(int));
    if (out == NULL) {
        return NULL;
    }
    for (int i = 0; i < na; i++) {
        out[i] = a[i];
    }
    for (int i = 0; i < nb; i++) {
        out[na + i] = b[i];        /* b's elements start after a's na slots */
    }
    return out;
}

int main(void) {
    int a[] = {1, 2, 3};
    int b[] = {4, 5};

    int *joined = concat(a, 3, b, 2);
    if (joined == NULL) {
        return 1;
    }
    for (int i = 0; i < 5; i++) {
        printf("%d ", joined[i]);
    }
    printf("\n");

    free(joined);
    return 0;
}
1 2 3 4 5
  • The size is computed at run time from na + nb, then allocated in one block. The second copy starts at offset na, so b lands right after a.
  • const int *a and const int *b say the inputs are read-only; only the new block out is written. The caller owns out and frees it.
  • This "compute the size, allocate, fill, return, caller frees" pattern is how C code builds data whose length is not known until the program runs - the same shape behind growing buffers and string builders.