C Concepts Visualization

Python Tutor’s real power is making the invisible visible: memory addresses, stack frames, heap boxes, pointer arrows, and lifetime of variables — things that are abstract when you read C code but concrete when you watch the runtime step by step.

Each section below presents a small, self-contained C program paired with an interactive Python Tutor visualization. Click through the steps and watch the concept unfold in real time.

Stack Frames and Local Variables

Every function call creates a new stack frame — a private region of memory holding that function’s local variables and return address. When the function returns, its frame is discarded. Step through the visualization below and watch the call stack grow (add, then mul) and shrink back to main.

/* Visualization: each function call creates a new stack frame with its own locals */
#include <stdio.h>
int add(int a, int b) { return a + b; }
int mul(int x, int y) { return x * y; }
int main(void) {
    int p = add(2, 3);  /* new frame for add: a=2, b=3 */
    int q = mul(p, 4);  /* new frame for mul: x=5, y=4 */
    printf("%d\n", q);
    return 0;
}

Pointer as a Memory Address

A pointer is just an integer that holds a memory address. Python Tutor draws it as an arrow from the pointer variable to the box it points at. Watch how *p = 99 changes x — the arrow tells you why.

/* Visualization: a pointer IS a memory address; * follows the arrow to the value */
#include <stdio.h>
int main(void) {
    int x = 42;
    int *p = &x;    /* p holds the address of x */
    printf("x=%d  *p=%d\n", x, *p);
    *p = 99;        /* writing through pointer changes x */
    printf("x=%d  *p=%d\n", x, *p);
    return 0;
}

Array and Pointer Arithmetic

a[i] and *(p+i) compile to the exact same machine instruction. The array name a decays to a pointer to its first element. Step through to see p advance by sizeof(int) with each increment, landing on successive elements.

/* Visualization: a[i] and *(p+i) are identical — pointer arithmetic on arrays */
#include <stdio.h>
int main(void) {
    int a[4] = {10, 20, 30, 40};
    int *p = a;     /* p points to first element */
    int i;
    for (i = 0; i < 4; i++)
        printf("a[%d]=%d  *(p+%d)=%d\n", i, a[i], i, *(p+i));
    return 0;
}

String as a Null-Terminated Character Array

In C a string is just a char array whose last byte is '\0' (ASCII 0). Every standard library function that walks a string — strlen, strcpy, printf with %s — stops when it finds that zero byte. Watch the loop reveal each character and finally the null terminator.

/* Visualization: a string is a char array ending with '\0' (value 0) */
#include <stdio.h>
int main(void) {
    char s[] = "hi!";
    int i = 0;
    while (s[i]) {
        printf("s[%d] = '%c'  (%d)\n", i, s[i], (int)s[i]);
        i++;
    }
    printf("s[%d] = '\\0' (%d)  <- null terminator\n", i, (int)s[i]);
    return 0;
}

Pass by Value vs Pass by Reference

C passes arguments by value: the called function receives copies, not the originals. swap_wrong swaps its local copies; the caller’s x and y are untouched. swap_right receives addresses, so writes through *a and *b reach the caller’s variables. Step through both calls and compare the frames.

/* Visualization: swap_wrong gets copies; swap_right gets addresses — sees original */
#include <stdio.h>
void swap_wrong(int a, int b)   { int t = a; a = b; b = t; }
void swap_right(int *a, int *b) { int t = *a; *a = *b; *b = t; }
int main(void) {
    int x = 3, y = 7;
    swap_wrong(x, y);
    printf("after swap_wrong: x=%d y=%d\n", x, y); /* unchanged */
    swap_right(&x, &y);
    printf("after swap_right: x=%d y=%d\n", x, y); /* swapped */
    return 0;
}

Linked List and Heap Allocation

malloc allocates memory on the heap — separate from the stack. Each call returns the address of a fresh node struct. Linking them via next pointers creates a chain. Python Tutor renders each heap object as a box with arrows showing the chain from abc.

/* Visualization: malloc creates heap boxes; next pointers chain them */
#include <stdio.h>
#include <stdlib.h>
struct node { int val; struct node *next; };
int main(void) {
    struct node *a = malloc(sizeof *a); a->val = 1; a->next = NULL;
    struct node *b = malloc(sizeof *b); b->val = 2; b->next = NULL;
    struct node *c = malloc(sizeof *c); c->val = 3; c->next = NULL;
    a->next = b;
    b->next = c;
    struct node *p = a;
    while (p) { printf("%d\n", p->val); p = p->next; }
    return 0;
}

Recursive Call Stack Unwinding

Each recursive call to fact(n) pushes a new frame onto the call stack, each holding its own n. When the base case n <= 1 returns 1, the stack unwinds: each pending frame multiplies its n into the result as frames pop off. Watch the stack grow four deep, then collapse.

/* Visualization: each recursive call pushes a new frame; base case unwinds them */
#include <stdio.h>
int fact(int n) {
    if (n <= 1) return 1;
    return n * fact(n - 1);
}
int main(void) {
    printf("4! = %d\n", fact(4));
    return 0;
}

Stack Data Structure: sp Index and val[] Array

The RPN calculator from K&R Chapter 4 implements a stack using a global index sp into a double val[] array. push stores a value at val[sp] and advances sp; pop retreats sp and returns the value. Step through to see sp tick up and down as values are pushed and popped.

/* Visualization: sp index advances/retreats in val[] — the LIFO stack in action */
#include <stdio.h>
int sp = 0;
double val[4];
void push(double v) { val[sp++] = v; }
double pop(void)    { return val[--sp]; }
int main(void) {
    push(1.0); push(2.0); push(3.0);
    printf("pop=%.0f\n", pop()); /* 3 */
    printf("pop=%.0f\n", pop()); /* 2 */
    push(9.0);
    printf("pop=%.0f\n", pop()); /* 9 */
    printf("pop=%.0f\n", pop()); /* 1 */
    return 0;
}

Struct Memory Layout and Nested Structs

A struct lays its fields out in memory consecutively (with possible padding for alignment). Nested structs are embedded inline — no extra indirection. The rect containing two point structs occupies exactly 2 * sizeof(point) bytes. Python Tutor shows the nested boxes side by side.

/* Visualization: struct fields at consecutive addresses; nested structs compose */
#include <stdio.h>
struct point { int x; int y; };
struct rect  { struct point ul; struct point lr; };
int main(void) {
    struct rect r = {{1, 2}, {5, 6}};
    printf("ul=(%d,%d)  lr=(%d,%d)\n", r.ul.x, r.ul.y, r.lr.x, r.lr.y);
    printf("sizeof(point)=%zu  sizeof(rect)=%zu\n",
           sizeof(struct point), sizeof(struct rect));
    return 0;
}

Union: Multiple Interpretations of the Same Bytes

A union’s fields all start at the same address — the union is exactly as large as its largest member. Writing the int field and reading back through the char bytes[] field shows you the raw byte representation of the integer in memory (endian-order). Python Tutor shows one memory box shared by both fields.

/* Visualization: union fields share the same bytes — write int, read as chars */
#include <stdio.h>
union u { int i; char bytes[4]; };
int main(void) {
    union u x;
    x.i = 0x41424344;   /* stores 'D','C','B','A' in bytes (little-endian) */
    printf("as int:  0x%08X\n", (unsigned)x.i);
    int j;
    for (j = 0; j < 4; j++)
        printf("bytes[%d] = 0x%02X  '%c'\n", j,
               (unsigned char)x.bytes[j], x.bytes[j]);
    return 0;
}

Static Variable: Persistent Local State

A static local variable is stored in the global (static) memory region, not on the stack. It is initialized once and retains its value across calls. Python Tutor shows n living in the global frame while counter’s stack frame comes and goes with each call.

/* Visualization: static n lives in global frame across calls; local would reset */
#include <stdio.h>
int counter(void) {
    static int n = 0;  /* persists between calls */
    n++;
    return n;
}
int main(void) {
    printf("%d\n", counter()); /* 1 */
    printf("%d\n", counter()); /* 2 */
    printf("%d\n", counter()); /* 3 */
    return 0;
}

Pointer to Pointer: Two Levels of Indirection

int **pp holds the address of a pointer, which in turn holds the address of an int. Python Tutor draws two arrows: pp p x. Dereferencing twice with **pp follows both arrows to reach x. This pattern is the foundation of pointer arrays, argv, and functions that modify a pointer.

/* Visualization: pp -> p -> x; two levels of indirection, two arrows to follow */
#include <stdio.h>
int main(void) {
    int x = 5;
    int *p   = &x;   /* p  points to x */
    int **pp = &p;   /* pp points to p */
    printf("x=%d  *p=%d  **pp=%d\n", x, *p, **pp);
    **pp = 99;       /* write through both levels of indirection */
    printf("x=%d\n", x);
    return 0;
}