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 a → b → c.
/* 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;
}
