C/C++ Arena

Step 9 of 10

Where a program's data lives

You've used three kinds of storage: locals, globals and static variables, and malloc memory. They live in different regions of the program's memory, and knowing the map explains most lifetime rules and many bugs. On a typical Linux or macOS program, the regions are:

Region What's in it Lifetime
text the machine code of your functions whole run, read-only
rodata string literals, const globals whole run, read-only
data globals and static variables with a non-zero initial value whole run
bss globals and static variables that start at zero whole run
heap blocks from malloc, until free until you free it
stack locals, parameters, return addresses until the function returns
#include <stdlib.h>

int high_score = 100;          /* data */
int players;                   /* bss: zero-initialized */
const char *title = "Arena";   /* the pointer is in data; the text "Arena" is in rodata */

int main(void) {
    static int calls;          /* bss, lives for the whole run */
    int round = 1;             /* stack */
    int *scores = malloc(10 * sizeof *scores);   /* the pointer is on the stack; the block is on the heap */
    free(scores);
    return 0;
}

Stack frames and calling conventions

Each function call pushes a stack frame holding the return address, saved registers and the function's locals, and pops it on return. That's why returning a pointer to a local is a bug: the frame, and the local in it, is gone.

How arguments get passed is fixed by the platform's calling convention, part of its ABI (application binary interface, the rules compiled code from different compilers must share). On x86-64 Linux and Intel Macs (the System V convention), the first six integer or pointer arguments travel in registers (rdi, rsi, rdx, rcx, r8, r9), the return value comes back in rax, and further arguments go on the stack. 64-bit Windows uses four registers (rcx, rdx, r8, r9), and 64-bit ARM (phones, Apple silicon) uses eight (x0 to x7, with the result in x0). You rarely deal with this yourself, but it's why calls with a few arguments are cheap, and it's what you see when you read assembly or a debugger's register view.

Your task

Each function below must return something that is still valid after it returns. make_scores(n) returns a new heap array of n zeros (the caller frees it), level_name(lvl) returns the name for levels 1 to 3 ("easy", "normal", "hard") or "unknown", and next_ticket() returns 1, 2, 3, ... on successive calls. Pick the right storage for each: heap, string literal (rodata), and a static local (bss).

Challenge 2

Write const char *label(int n) that returns "player N" for the number n. The text is built at runtime, so it can't be a string literal, and a local array would be gone after return. Use a static buffer and snprintf. The tests only use each result before the next call, because the next call overwrites the same buffer.

Challenge 3

Write double running_average(int x) that remembers every value it has been given and returns the average of all of them so far. The first call with 10 returns 10, then 20 returns 15, then 60 returns 30. Use static locals, not globals.

Previous: Who owns what Next: Challenge: a string builder