Skip to content

Handout: Lecture 6 In-Class Exercises

Part A is pen-and-paper: trace a recursion's stack frames and spot a dangling-pointer bug by reading. Part B is at the keyboard: allocate memory on the heap, use it, and free it - first a dynamic array, then a function that hands a heap array back to its caller. None of these repeats an earlier exercise; they drill the new move - malloc it, check it, use it, free it once.

Try each one yourself first; we will discuss in class, and the solutions are in a separate document afterward. Compile with warnings on, and link <stdlib.h> for malloc/free:

clang -Wall -Wextra -std=c17 myprog.c -o myprog

Reminders that will keep you out of trouble today:

  • A local variable lives only while its stack frame is on the stack - it dies when the function returns. Never return the address of a local.
  • Every malloc has exactly one free. Zero frees leaks; two frees is a double-free. Pair them as you write the malloc.
  • Check malloc's result against NULL before you dereference it.
  • After free(p), the block is no longer yours. Set p = NULL; so a stray use crashes loudly instead of corrupting memory.
  • Size an allocation as count times element size: malloc(n * sizeof(int)). Never hard-code the byte count.

Set up

mkdir -p ~/cmsc14300/lec06
cd ~/cmsc14300/lec06

Part A - Stacks and lifetimes (pen and paper)

Exercise A1 - Trace the frames

Do not compile this; trace it by hand. Consider:

int sum_to(int n) {
    if (n == 0) {
        return 0;
    }
    return n + sum_to(n - 1);
}
  1. Draw the stack frames for sum_to(3). How many frames are on the stack at the deepest point, and what is each frame's n?
  2. In what order do the frames return? Write the value each one returns.
  3. Each call has its own n. Explain in one sentence why the three n values do not collide.

Exercise A2 - Spot the bug

Each snippet has one memory bug. Name it (dangling pointer / use-after-free / double-free / memory leak / returning a local's address) and say how to fix it.

/* (a) */
int *first(void) {
    int x = 10;
    return &x;
}

/* (b) */
void run(void) {
    int *p = malloc(sizeof(int));
    *p = 5;
    free(p);
    printf("%d\n", *p);
}

/* (c) */
void load(int n) {
    int *a = malloc(n * sizeof(int));
    /* ... fills and uses a ... */
    return;
}

Part B - The heap at the keyboard

Exercise B1 - A dynamic array

Read a count n at run time, allocate exactly n ints on the heap, fill them from input, report something about them, and free the block. This is the array Lecture 4 could not give you - its size is chosen while the program runs.

How many numbers? 4
Enter 4 numbers: 
10 
25 
30 
7
largest: 30
  • Allocate with int *a = malloc(n * sizeof(int)); and check it against NULL before using it.
  • Fill with a scanf("%d", &a[i]) loop, then walk the array to find the largest (seed from a[0]).
  • End with exactly one free(a);. There is one malloc, so there is one free.
  • Stretch: also report the average as a double (cast before dividing). The two facts - a runtime size and a heap block - are the only new things here; the array logic is the same as always.

Exercise B2 - Return a heap array

Write a function that builds and returns a heap array of the first n squares, then have main use it and free it. This is the safe version of "returning a local's address": the heap block outlives the call.

int *make_squares(int n);   /* returns a heap array: 0, 1, 4, 9, ... */
0 1 4 9 16
  • Inside make_squares: malloc(n * sizeof(int)), check for NULL (return NULL on failure), fill a[i] = i * i;, and return a;.
  • In main: call it, check the result, print the values, then free it - main now owns the block. The function allocated it; the caller frees it.
  • Stretch: write the broken sibling int *make_squares_bad(int n) that declares a local int a[100]; and returns a;. Note the compiler warning about returning the address of a local, and explain why B2's heap version is safe where this one is not.

Stretch - take it further

Concatenate two arrays onto the heap

Write a function that allocates a brand-new array big enough to hold two input arrays end to end, copies them in, and returns it.

int *concat(const int *a, int na, const int *b, int nb);   /* length na + nb */
  • Allocate (na + nb) * sizeof(int), check for NULL, copy a's na elements then b's nb elements into the new block, and return it.
  • The caller owns the result and must free it. Print the joined array from main to check, e.g. {1, 2, 3} and {4, 5} give 1 2 3 4 5.
  • The const on the inputs promises concat only reads them; only the new block is written. This "allocate, fill, return, caller frees" shape is how real C code builds up data whose size it computes at run time.