Visualizing Memory: Box-and-Arrow Diagrams
- Purpose: provide an intuitive, spatial view of how C variables live in memory (names, values, addresses, pointer relationships).
- Demonstrated with small
main
containing:- Scalar (
int x = 1
) - Fixed array (
int arr[3] = {2,3,4}
) - Pointer into the array (
int *p = &arr[1]
)
- Key observations shown by successive slides (Pages 2-6):
- Local variables reside in one contiguous stack frame; addresses decrease upward (typical x86).
- Consecutive array elements are laid out contiguously; pointer
p
stores address of middle element. - Printing addresses with
%p
illustrates pointer values; printing contents with %d
confirms data. - A stack frame diagram labels columns “name”, “value”, “address”.
- Helps answer: “Where is a pointer variable itself stored vs. where does it point?”
Double Pointers (**
)
- Concept: pointer to a pointer; enables indirect updates of the pointer itself.
- Example
exercise0.c
(Pages 7 & 15):char hi[6] = "hello\0";
char *p = &hi[0];
char **dp = &p;
( dp
points to p
, which points to first element).
- Execution steps:
*p
and **dp
both yield 'h'
because they ultimately dereference to same byte.- After
p += 1
, pointer moves to 'e'
; but *dp
still unchanged (points to old address), so *p
≠**dp
. *dp += 2;
modifies the pointer through double indirection; now both point to 'l'
.
- Take-away:
char *
vs char **
differ in level of indirection: the latter lets you change where the former points.
Pointer Arithmetic (Typed Scaling)
- Rule: adding n to pointer of type T* advances by n\times\text{sizeof}(T) bytes.
- Demonstration program
pointerarithmetic.c
(Pages 17-31):int *int_ptr
vs char *char_ptr
pointing to same address initially.- On 32-bit little-endian x86,
int
is 4 bytes so int_ptr+1
steps 4 bytes; char_ptr+1
steps 1 byte. - Reading beyond array bounds shows garbage (
-1073745224
)—undefined behaviour.
- Illustrated memory dump: byte sequence
01 00 00 00 02 00 00 00 03 00 00 00
and how each pointer interpret/strides through it. - Equation: \text{new_address}=\text{old_address}+k\times\text{sizeof}(\text{referenced type})
Buffers: Definition & Typeless Pointers
- Buffer = temporary holding place; simply a region of memory often accessed through a pointer.
void *
used for generic buffers (compiler lacks type info). Cast required before dereference.- Example (Page 33): cast to
char *
yields 'a'
; cast to int32_t *
interprets 4 bytes as integer 1684234849 ( ASCII "abcd" ).
Copy / Fill / Compare Routines
memcpy(dest,src,n)
– byte-wise copy; no terminator; overlapping undefined.- Examples: whole-buffer copy; buffer splicing by passing
&buf1[2]
as destination.
memset(buf,c,n)
– fills n bytes with constant c\&(0xFF).- Beware when filling non-byte arrays:
int b[4]; memset(b,0,4);
only zeroes first element!
memcmp(buf1,buf2,n)
– lexical byte comparison; returns
Memory Classes in C
- Static (globals): allocated at load, freed at process exit (
int counter
). - Automatic (stack): lifetime is function call (
int x;
). - Dynamic / Heap: manual management via allocator.
- Needed when: lifetime not tied to call, size unknown, or too large for stack.
Dynamic Allocation API
- void *malloc(size_t\ size); – raw bytes, uninitialised.
- void *calloc(size_t\ n, size_t\ sz); – zero-initialised n elements.
- void free(void *ptr); – releases; good practice:
ptr=NULL;
after call to avoid *dangling pointer*. - void *realloc(void *ptr,size_t\ new_size); – resize in place if possible, else move & copy, returns new pointer.
Example Patterns
- Allocate struct (Page 46):
typedef struct { double real, imag; } Complex;
Complex *AllocComplex(double r,double i){
Complex *p=malloc(sizeof(Complex));
if(p){ p->real=r; p->imag=i; }
return p;
}
- Array copy demo (
arraycopy.c
, Pages 49-60): allocate inside helper, return pointer, free
in caller.
Heap & Process Address Space
- Segments (low→high addresses):
- Text (*.text, *.rodata) – code, constants (read-only)
- Data/BSS – global vars
- Heap – grows upward via program break
- Shared libs
- Stack – grows downward
malloc
/free
implemented in libc using brk()
/ sbrk()
or anonymous mmap()
to move program break.- Example (Page 73): calling
malloc(0x1000)
1024 times raised break by 4\,190\,208\ (≈4\,096\,000) bytes, freeing lowered it.
NULL Pointer
- Defined as 0x0 on Linux; guaranteed invalid. Dereference → segmentation fault.
- Best practice: set pointer to
NULL
immediately after free
to fail fast.
Memory Hazards
- Corruption: out-of-bounds writes (
b[2]=5
), misuse after free
, wrong pointer arithmetic, double free
, freeing non-heap pointer. - Leak: forgetting to
free
unreachable block (Page 64). Long-running processes may exhaust memory – potential DoS. - In rare scenarios leaking is safer than corrupted
free
.
- Purify, Valgrind, AddressSanitizer (
gcc -fsanitize=address
) – detect leaks, use-after-free, buffer over/underflow.
Manual vs Automatic Memory Management
- C/C++/Rust: manual (Rust has borrow-checker aiding safety).
- Java/Python/Go: garbage collection performs reachability analysis; pros (safety) vs cons (pause/unpredictability).
Summary Cheat-Sheet
- Allocation family (Page 67):
malloc
, calloc
, realloc
, free
. All live in user-space; many alternative allocators (dlmalloc, tcmalloc, jemalloc). - Pointer arithmetic obeys element size.
- Use
memcpy
, memset
, memcmp
for raw byte operations; avoid mixing with string routines unless NUL-terminated. - Always match every
malloc/calloc/realloc
with one free
. - After
free
: ptr=NULL;
→ safer crashes. - Employ sanitizers in development & CI to catch leaks/corruption early.