Understanding Functions, Memory, and Pointers in C: A Complete Guide
Part 1: Functions and the Call Stack
Before we can understand pointers and memory management in C, we need to understand how functions work and what happens behind the scenes when your program runs. This foundation will make everything else click into place.
1.1 What is a Function?
A function is a self-contained block of code that performs a specific task. Think of it as a mini-program within your program. Functions help us:
Organize code into logical, manageable pieces
Reuse code instead of writing the same logic multiple times
Abstract complexity by hiding implementation details behind a simple name
Anatomy of a C Function
/*
* Every C function has four main parts:
* 1. Return type - What kind of value the function gives back
* 2. Function name - How we refer to this function
* 3. Parameters - What information the function needs to do its job
* 4. Function body - The actual code that runs when the function is called
*/
// (1) (2) (3)
// | | |
// v v v
int add (int a, int b) // <-- This line is called the "function signature"
{ // <-- Opening brace: start of function body (4)
int sum = a + b; // <-- Local variable declaration and computation
return sum; // <-- Return statement: sends a value back to the caller
} // <-- Closing brace: end of function body
Let's break this down further:
| Component | In Our Example | Purpose |
| Return type | int | The function will give back an integer value |
| Function name | add | We'll use this name to call the function |
| Parameters | int a, int b | The function expects two integers as input |
| Function body | { ... } | The code that executes when the function runs |
A Complete Example: Your First Function
/*
* File: first_function.c
*
* This program demonstrates a simple function that adds two numbers.
* We'll trace through exactly what happens when this program runs.
*/
#include <stdio.h> // Required for printf() function
/*
* Function: add
* -------------
* Computes the sum of two integers.
*
* Parameters:
* a - the first integer to add
* b - the second integer to add
*
* Returns:
* The sum of a and b as an integer
*/
int add(int a, int b)
{
/*
* Step 1: Create a local variable called 'sum'
* This variable exists ONLY inside this function.
* It will be destroyed when the function ends.
*/
int sum = a + b;
/*
* Step 2: Return the computed value
* This sends the value of 'sum' back to whoever called this function.
* After this line, the function ends and 'sum' no longer exists.
*/
return sum;
}
/*
* Function: main
* --------------
* The entry point of every C program.
* This is where execution begins.
*/
int main()
{
/*
* Step 1: Declare two integer variables and assign values to them.
* These are "local variables" - they exist only inside main().
*/
int x = 5;
int y = 3;
/*
* Step 2: Call the add() function
* - The VALUES of x and y (5 and 3) are COPIED to parameters a and b
* - The add() function executes
* - The returned value (8) is stored in our new variable 'result'
*/
int result = add(x, y);
/*
* Step 3: Print the result to the screen
* %d is a format specifier meaning "print an integer here"
*/
printf("The sum of %d and %d is %d\n", x, y, result);
/*
* Step 4: Return 0 to indicate the program completed successfully
* By convention, returning 0 from main() means "no errors"
*/
return 0;
}
Output:
The sum of 5 and 3 is 8
1.2 The Call Stack Explained
Now here's where it gets interesting. When your program runs, how does the computer keep track of:
Which function is currently executing?
What are the values of local variables?
Where should it go back to when a function finishes?
The answer is the call stack.
What is a Stack?
A stack is a data structure that follows the LIFO principle: Last In, First Out.
Think of it like a stack of plates:
You can only add a plate to the top (called "push")
You can only remove the plate from the top (called "pop")
You cannot access plates in the middle without removing the ones above
STACK OF PLATES COMPUTER STACK
--------------- --------------
| Plate 3 | <-- Top | Function C | <-- Top (currently executing)
| Plate 2 | | Function B |
| Plate 1 | <-- Bottom | Function A |
|___________| | main | <-- Bottom (started first)
The Call Stack in Action
When a function is called, the computer creates a stack frame (also called an "activation record") and pushes it onto the call stack. This frame contains:
Return Address - Where to continue executing after this function ends
Parameters - Copies of the values passed to the function
Local Variables - Variables declared inside the function
Saved Registers - (Advanced) CPU state that needs to be preserved
┌─────────────────────────────────────┐
│ STACK FRAME │
├─────────────────────────────────────┤
│ Return Address │ ← Where to go when function ends
├─────────────────────────────────────┤
│ Parameters (a, b, c, ...) │ ← Copies of values passed in
├─────────────────────────────────────┤
│ Local Variables (x, y, z, ...) │ ← Variables declared in function
├─────────────────────────────────────┤
│ Saved Registers (advanced) │ ← CPU bookkeeping
└─────────────────────────────────────┘
The Stack Grows Downward
In most systems, the stack grows from high memory addresses toward low memory addresses. This is a historical design choice. When we say the stack "grows," we mean it expands downward in memory.
Memory Address
0xFFFF (High) ┌─────────────┐
│ │
│ STACK │ ← Grows DOWNWARD
│ ↓ │
│ │
├─────────────┤
│ │
│ (free) │
│ │
├─────────────┤
│ ↑ │
│ HEAP │ ← Grows UPWARD
│ │
0x0000 (Low) └─────────────┘
1.3 Tracing Through a Function Call: Step by Step
Let's trace through exactly what happens when we call a function. We'll use a more elaborate example with multiple function calls.
/*
* File: stack_trace.c
*
* This program demonstrates how the call stack works
* by calling multiple functions in sequence.
*/
#include <stdio.h>
/*
* Function: multiply
* ------------------
* Multiplies two integers together.
*/
int multiply(int m, int n)
{
int product = m * n; // Local variable 'product'
return product;
}
/*
* Function: add_and_double
* ------------------------
* Adds two numbers together, then doubles the result.
* This function calls multiply() to do the doubling.
*/
int add_and_double(int a, int b)
{
int sum = a + b; // Local variable 'sum'
int doubled = multiply(sum, 2); // Call multiply() with sum and 2
return doubled;
}
/*
* Function: main
* --------------
* Program entry point.
*/
int main()
{
int x = 3;
int y = 4;
int result = add_and_double(x, y); // Call add_and_double() with 3 and 4
printf("Result: %d\n", result);
return 0;
}
Output:
Result: 14
Let's trace through this program step by step, watching the stack grow and shrink.
Step 1: Program Starts - main() is Called
When the program begins, the operating system calls main(). A stack frame for main() is created.
CALL STACK (Step 1)
═══════════════════════════════════════════
┌─────────────────────────────────────────┐
│ main() │ ← Currently executing
├─────────────────────────────────────────┤
│ Return Address: [to OS] │
│ Local Variables: │
│ x = 3 │
│ y = 4 │
│ result = ??? (not yet assigned) │
└─────────────────────────────────────────┘
Stack Bottom (main is always at the bottom)
Step 2: main() Calls add_and_double(3, 4)
When we reach the line int result = add_and_double(x, y);:
The values 3 and 4 are copied to parameters
aandbA new stack frame for
add_and_double()is pushed onto the stackExecution jumps to
add_and_double()
CALL STACK (Step 2)
═══════════════════════════════════════════
┌─────────────────────────────────────────┐
│ add_and_double() │ ← Currently executing
├─────────────────────────────────────────┤
│ Return Address: [back to main, line │
│ where we called this] │
│ Parameters: │
│ a = 3 (COPY of x) │
│ b = 4 (COPY of y) │
│ Local Variables: │
│ sum = 7 │
│ doubled = ??? (not yet assigned) │
└─────────────────────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ main() │ ← Waiting for add_and_double to return
├─────────────────────────────────────────┤
│ Return Address: [to OS] │
│ Local Variables: │
│ x = 3 │
│ y = 4 │
│ result = ??? (waiting) │
└─────────────────────────────────────────┘
Stack Bottom
Step 3: add_and_double() Calls multiply(7, 2)
Inside add_and_double(), we reach int doubled = multiply(sum, 2);:
The values 7 and 2 are copied to parameters
mandnA new stack frame for
multiply()is pushed onto the stackExecution jumps to
multiply()
CALL STACK (Step 3) - STACK AT ITS DEEPEST
═══════════════════════════════════════════
┌─────────────────────────────────────────┐
│ multiply() │ ← Currently executing
├─────────────────────────────────────────┤
│ Return Address: [back to add_and_double│
│ where we called this] │
│ Parameters: │
│ m = 7 (COPY of sum) │
│ n = 2 │
│ Local Variables: │
│ product = 14 │
└─────────────────────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ add_and_double() │ ← Waiting
├─────────────────────────────────────────┤
│ Return Address: [back to main] │
│ Parameters: │
│ a = 3 │
│ b = 4 │
│ Local Variables: │
│ sum = 7 │
│ doubled = ??? (waiting) │
└─────────────────────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ main() │ ← Waiting
├─────────────────────────────────────────┤
│ Local Variables: │
│ x = 3 │
│ y = 4 │
│ result = ??? (waiting) │
└─────────────────────────────────────────┘
Stack Bottom
Step 4: multiply() Returns 14
When multiply() executes return product;:
The value 14 is sent back to the caller
The stack frame for
multiply()is destroyed (popped off the stack)All local variables in
multiply()(m,n,product) no longer existExecution continues in
add_and_double()right where it left off
CALL STACK (Step 4) - multiply() frame is GONE
═══════════════════════════════════════════
┌─────────────────────────────────────────┐
│ add_and_double() │ ← Currently executing again
├─────────────────────────────────────────┤
│ Return Address: [back to main] │
│ Parameters: │
│ a = 3 │
│ b = 4 │
│ Local Variables: │
│ sum = 7 │
│ doubled = 14 ← Now has a value! │
└─────────────────────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ main() │ ← Still waiting
├─────────────────────────────────────────┤
│ Local Variables: │
│ x = 3 │
│ y = 4 │
│ result = ??? (waiting) │
└─────────────────────────────────────────┘
Stack Bottom
╔═══════════════════════════════════════════╗
║ multiply()'s frame has been DESTROYED! ║
║ The variables m, n, and product ║
║ NO LONGER EXIST in memory. ║
╚═══════════════════════════════════════════╝
Step 5: add_and_double() Returns 14
When add_and_double() executes return doubled;:
The value 14 is sent back to
main()The stack frame for
add_and_double()is destroyedExecution continues in
main()
CALL STACK (Step 5) - add_and_double() frame is GONE
═══════════════════════════════════════════
┌─────────────────────────────────────────┐
│ main() │ ← Currently executing again
├─────────────────────────────────────────┤
│ Return Address: [to OS] │
│ Local Variables: │
│ x = 3 │
│ y = 4 │
│ result = 14 ← Now has a value! │
└─────────────────────────────────────────┘
Stack Bottom
Step 6: main() Returns 0
When main() executes return 0;:
The value 0 is returned to the operating system
The stack frame for
main()is destroyedThe program ends
CALL STACK (Step 6) - EMPTY
═══════════════════════════════════════════
(Stack is empty - program has ended)
1.4 Key Takeaways from Part 1
Let's summarize the crucial concepts we've learned:
1. Functions Organize and Reuse Code
// Instead of writing the same addition logic everywhere:
int sum1 = 5 + 3;
int sum2 = 10 + 20;
int sum3 = 7 + 8;
// We write it once and reuse it:
int sum1 = add(5, 3);
int sum2 = add(10, 20);
int sum3 = add(7, 8);
2. The Stack is LIFO (Last In, First Out)
The most recently called function is always on top
Functions return in the opposite order they were called
If A calls B calls C, they return in order: C, then B, then A
3. Each Function Call Gets Its Own Stack Frame
Parameters are copies of the values passed in
Local variables exist only within that frame
When the function returns, the frame is destroyed
4. Variables Have Scope and Lifetime
Scope: Where in the code a variable can be accessed (inside its function)
Lifetime: How long the variable exists (from function call to function return)
int add(int a, int b) // a and b are created here
{
int sum = a + b; // sum is created here
return sum;
} // a, b, and sum are ALL destroyed here
Part 2: Recursion and the Stack
Now that you understand how the call stack works with regular function calls, let's explore one of the most fascinating (and sometimes confusing) concepts in programming: recursion. Understanding recursion will deepen your knowledge of the stack and prepare you for understanding memory-related bugs later.
2.1 What is Recursion?
Recursion is when a function calls itself. That's it. Simple definition, but the implications are profound.
Every recursive function has two essential components:
Base Case - The condition that stops the recursion (without this, you get infinite recursion!)
Recursive Case - The part where the function calls itself with a "smaller" or "simpler" problem
Think of recursion like Russian nesting dolls (Matryoshka): each doll contains a smaller version of itself, until you reach the smallest doll that contains nothing.
RUSSIAN NESTING DOLLS RECURSIVE FUNCTION CALLS
──────────────────── ────────────────────────
┌─────────────────┐ factorial(4)
│ ┌───────────┐ │ │
│ │ ┌─────┐ │ │ ├── factorial(3)
│ │ │ ┌─┐ │ │ │ │ │
│ │ │ │•│ │ │ │ ← Smallest │ ├── factorial(2)
│ │ │ └─┘ │ │ │ (base case) │ │ │
│ │ └─────┘ │ │ │ │ ├── factorial(1)
│ └───────────┘ │ │ │ │ │
└─────────────────┘ │ │ │ └── returns 1 (base case!)
│ │ └── returns 2
│ └── returns 6
└── returns 24
2.2 The Classic Example: Factorial
The factorial of a number n (written as n!) is the product of all positive integers from 1 to n.
5! = 5 × 4 × 3 × 2 × 1 = 120
4! = 4 × 3 × 2 × 1 = 24
3! = 3 × 2 × 1 = 6
2! = 2 × 1 = 2
1! = 1
0! = 1 (by definition)
Notice a pattern? We can define factorial recursively:
n! = n × (n-1)!
For example:
5! = 5 × 4!
4! = 4 × 3!
3! = 3 × 2!
2! = 2 × 1!
1! = 1 ← Base case: we just know this answer
Factorial in C: Iterative vs Recursive
Let's see both approaches side by side:
/*
* File: factorial_comparison.c
*
* This program shows both iterative and recursive approaches
* to calculating factorial. Both produce the same result,
* but they work very differently internally.
*/
#include <stdio.h>
/*
* Function: factorial_iterative
* -----------------------------
* Calculates n! using a loop (iteration).
*
* This approach:
* - Uses a single stack frame
* - Loops through all numbers from 1 to n
* - Multiplies them together one by one
*
* Parameters:
* n - the number to calculate factorial of (must be >= 0)
*
* Returns:
* n! (n factorial)
*/
int factorial_iterative(int n)
{
int result = 1; // Start with 1 (since 0! = 1 and 1! = 1)
/*
* Loop from 2 up to n, multiplying as we go.
* We start at 2 because multiplying by 1 doesn't change anything.
*/
for (int i = 2; i <= n; i++)
{
result = result * i; // Accumulate the product
}
return result;
}
/*
* Function: factorial_recursive
* -----------------------------
* Calculates n! using recursion.
*
* This approach:
* - Creates a NEW stack frame for each call
* - Breaks the problem into smaller pieces
* - Relies on the base case to stop
*
* The mathematical definition:
* - Base case: 0! = 1 and 1! = 1
* - Recursive case: n! = n × (n-1)!
*
* Parameters:
* n - the number to calculate factorial of (must be >= 0)
*
* Returns:
* n! (n factorial)
*/
int factorial_recursive(int n)
{
/*
* BASE CASE: The condition that stops the recursion.
* Without this, the function would call itself forever!
*
* We know that 0! = 1 and 1! = 1, so we can just return 1.
*/
if (n <= 1)
{
return 1; // Stop recursing! We know the answer.
}
/*
* RECURSIVE CASE: Break the problem into a smaller piece.
*
* To calculate n!, we need to calculate (n-1)! first,
* then multiply the result by n.
*
* This line does THREE things:
* 1. Calls factorial_recursive(n-1) - which creates a new stack frame
* 2. Waits for that call to return with a value
* 3. Multiplies n by that returned value
*/
return n * factorial_recursive(n - 1);
}
/*
* Function: main
* --------------
* Tests both factorial implementations.
*/
int main()
{
int number = 5;
printf("Calculating %d! (factorial of %d)\n\n", number, number);
/*
* Both functions should give the same answer: 120
*/
printf("Iterative: %d! = %d\n", number, factorial_iterative(number));
printf("Recursive: %d! = %d\n", number, factorial_recursive(number));
return 0;
}
Output:
Calculating 5! (factorial of 5)
Iterative: 5! = 120
Recursive: 5! = 120
2.3 Visualizing the Recursive Stack
Here's where your understanding of the call stack really pays off. Let's trace through factorial_recursive(4) step by step.
The Call Phase: Stack Growing
When we call factorial_recursive(4), here's what happens:
Call 1: factorial_recursive(4)
Is
4 <= 1? NOSo we need to calculate
4 * factorial_recursive(3)But wait! We can't finish this calculation until
factorial_recursive(3)returnsCreate a new stack frame and call
factorial_recursive(3)
CALL STACK - After Call 1
═══════════════════════════════════════════
┌─────────────────────────────────────────┐
│ factorial_recursive(4) │ ← Waiting for factorial(3)
├─────────────────────────────────────────┤
│ Parameter: n = 4 │
│ Needs to compute: 4 * factorial(3) │
│ Status: WAITING │
└─────────────────────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ main() │
└─────────────────────────────────────────┘
Call 2: factorial_recursive(3)
Is
3 <= 1? NOSo we need to calculate
3 * factorial_recursive(2)Create a new stack frame and call
factorial_recursive(2)
CALL STACK - After Call 2
═══════════════════════════════════════════
┌─────────────────────────────────────────┐
│ factorial_recursive(3) │ ← Waiting for factorial(2)
├─────────────────────────────────────────┤
│ Parameter: n = 3 │
│ Needs to compute: 3 * factorial(2) │
│ Status: WAITING │
└─────────────────────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ factorial_recursive(4) │ ← Waiting for factorial(3)
├─────────────────────────────────────────┤
│ Parameter: n = 4 │
│ Needs to compute: 4 * factorial(3) │
│ Status: WAITING │
└─────────────────────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ main() │
└─────────────────────────────────────────┘
Call 3: factorial_recursive(2)
Is
2 <= 1? NOSo we need to calculate
2 * factorial_recursive(1)Create a new stack frame and call
factorial_recursive(1)
CALL STACK - After Call 3
═══════════════════════════════════════════
┌─────────────────────────────────────────┐
│ factorial_recursive(2) │ ← Waiting for factorial(1)
├─────────────────────────────────────────┤
│ Parameter: n = 2 │
│ Needs to compute: 2 * factorial(1) │
│ Status: WAITING │
└─────────────────────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ factorial_recursive(3) │ ← WAITING
├─────────────────────────────────────────┤
│ Parameter: n = 3 │
└─────────────────────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ factorial_recursive(4) │ ← WAITING
├─────────────────────────────────────────┤
│ Parameter: n = 4 │
└─────────────────────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ main() │
└─────────────────────────────────────────┘
Call 4: factorial_recursive(1) — THE BASE CASE!
Is
1 <= 1? YES!We've hit the base case!
Return 1 immediately. No more recursive calls needed.
CALL STACK - At Maximum Depth (Base Case Reached)
═══════════════════════════════════════════
┌─────────────────────────────────────────┐
│ factorial_recursive(1) │ ← BASE CASE HIT!
├─────────────────────────────────────────┤
│ Parameter: n = 1 │
│ 1 <= 1 is TRUE │
│ RETURNING: 1 │
└─────────────────────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ factorial_recursive(2) │ ← Waiting
├─────────────────────────────────────────┤
│ Parameter: n = 2 │
└─────────────────────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ factorial_recursive(3) │ ← Waiting
├─────────────────────────────────────────┤
│ Parameter: n = 3 │
└─────────────────────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ factorial_recursive(4) │ ← Waiting
├─────────────────────────────────────────┤
│ Parameter: n = 4 │
└─────────────────────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ main() │
└─────────────────────────────────────────┘
╔═══════════════════════════════════════════╗
║ STACK AT MAXIMUM DEPTH! ║
║ We have 5 stack frames: ║
║ main → factorial(4) → factorial(3) → ║
║ factorial(2) → factorial(1) ║
╚═══════════════════════════════════════════╝
2.4 Stack Unwinding: The Return Phase
Now comes the beautiful part: stack unwinding. The base case returns, and each waiting function can finally complete its calculation.
Return 1: factorial_recursive(1) returns 1
The stack frame for factorial_recursive(1) is destroyed. Execution returns to factorial_recursive(2).
STACK UNWINDING - Step 1
═══════════════════════════════════════════
factorial(1) returned 1
Its stack frame is now DESTROYED
┌─────────────────────────────────────────┐
│ factorial_recursive(2) │ ← Now executing!
├─────────────────────────────────────────┤
│ Parameter: n = 2 │
│ factorial(1) returned: 1 │
│ Computing: 2 * 1 = 2 │
│ RETURNING: 2 │
└─────────────────────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ factorial_recursive(3) │ ← Waiting
└─────────────────────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ factorial_recursive(4) │ ← Waiting
└─────────────────────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ main() │
└─────────────────────────────────────────┘
Return 2: factorial_recursive(2) returns 2
STACK UNWINDING - Step 2
═══════════════════════════════════════════
factorial(2) returned 2
Its stack frame is now DESTROYED
┌─────────────────────────────────────────┐
│ factorial_recursive(3) │ ← Now executing!
├─────────────────────────────────────────┤
│ Parameter: n = 3 │
│ factorial(2) returned: 2 │
│ Computing: 3 * 2 = 6 │
│ RETURNING: 6 │
└─────────────────────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ factorial_recursive(4) │ ← Waiting
└─────────────────────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ main() │
└─────────────────────────────────────────┘
Return 3: factorial_recursive(3) returns 6
STACK UNWINDING - Step 3
═══════════════════════════════════════════
factorial(3) returned 6
Its stack frame is now DESTROYED
┌─────────────────────────────────────────┐
│ factorial_recursive(4) │ ← Now executing!
├─────────────────────────────────────────┤
│ Parameter: n = 4 │
│ factorial(3) returned: 6 │
│ Computing: 4 * 6 = 24 │
│ RETURNING: 24 │
└─────────────────────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ main() │
└─────────────────────────────────────────┘
Return 4: factorial_recursive(4) returns 24
STACK UNWINDING - Step 4 (Complete!)
═══════════════════════════════════════════
factorial(4) returned 24
Its stack frame is now DESTROYED
┌─────────────────────────────────────────┐
│ main() │ ← Back to main!
├─────────────────────────────────────────┤
│ result = 24 │
└─────────────────────────────────────────┘
╔═══════════════════════════════════════════╗
║ RECURSION COMPLETE! ║
║ ║
║ The call chain was: ║
║ factorial(4) → factorial(3) → ║
║ factorial(2) → factorial(1) ║
║ ║
║ The return chain was: ║
║ 1 → 2 → 6 → 24 ║
║ ║
║ Each return value got multiplied: ║
║ 1 × 2 = 2 ║
║ 2 × 3 = 6 ║
║ 6 × 4 = 24 ║
╚═══════════════════════════════════════════╝
2.5 The Complete Picture: A Timeline
Here's a summary showing the entire recursion process as a timeline:
TIME ──────────────────────────────────────────────────────────────────►
PHASE 1: CALLING (Stack Growing) PHASE 2: RETURNING (Stack Unwinding)
───────────────────────────────── ──────────────────────────────────────
main() calls factorial(4)
│
└──► factorial(4) calls factorial(3)
│
└──► factorial(3) calls factorial(2)
│
└──► factorial(2) calls factorial(1)
│
└──► factorial(1)
BASE CASE!
returns 1
│
◄────────────────────────┘
factorial(2): 2 * 1 = 2
returns 2
│
◄───────────────────┘
factorial(3): 3 * 2 = 6
returns 6
│
◄───────────────────┘
factorial(4): 4 * 6 = 24
returns 24
│
◄───────────────┘
main() receives 24
2.6 Another Example: Fibonacci Numbers
Let's look at another classic recursive example that creates an even more interesting call pattern: the Fibonacci sequence.
The Fibonacci sequence: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, ...
Each number is the sum of the two preceding numbers:
fib(0) = 0
fib(1) = 1
fib(n) = fib(n-1) + fib(n-2) for n > 1
/*
* File: fibonacci.c
*
* Demonstrates recursive Fibonacci calculation.
* Note: This is intentionally inefficient to demonstrate recursion.
* In practice, you'd use iteration or memoization.
*/
#include <stdio.h>
/*
* Function: fibonacci
* -------------------
* Calculates the nth Fibonacci number recursively.
*
* The Fibonacci sequence: 0, 1, 1, 2, 3, 5, 8, 13, 21, ...
* Each number is the sum of the two before it.
*
* Mathematical definition:
* fib(0) = 0 (base case 1)
* fib(1) = 1 (base case 2)
* fib(n) = fib(n-1) + fib(n-2) (recursive case)
*
* Parameters:
* n - which Fibonacci number to calculate (0-indexed)
*
* Returns:
* The nth Fibonacci number
*
* WARNING: This recursive implementation is SLOW for large n!
* It's used here for educational purposes only.
* fib(n) makes roughly 2^n function calls!
*/
int fibonacci(int n)
{
/*
* BASE CASE 1: The 0th Fibonacci number is 0
*/
if (n == 0)
{
return 0;
}
/*
* BASE CASE 2: The 1st Fibonacci number is 1
*/
if (n == 1)
{
return 1;
}
/*
* RECURSIVE CASE: fib(n) = fib(n-1) + fib(n-2)
*
* This creates a TREE of recursive calls, not just a linear chain!
* For example, fib(5) calls both fib(4) AND fib(3).
* Each of those makes two more calls, and so on.
*/
return fibonacci(n - 1) + fibonacci(n - 2);
}
int main()
{
printf("Fibonacci sequence (first 10 numbers):\n");
/*
* Print fib(0) through fib(9)
*/
for (int i = 0; i < 10; i++)
{
printf("fib(%d) = %d\n", i, fibonacci(i));
}
return 0;
}
Output:
Fibonacci sequence (first 10 numbers):
fib(0) = 0
fib(1) = 1
fib(2) = 1
fib(3) = 2
fib(4) = 3
fib(5) = 5
fib(6) = 8
fib(7) = 13
fib(8) = 21
fib(9) = 34
The Fibonacci Call Tree
Unlike factorial, which creates a linear chain of calls, Fibonacci creates a tree of calls:
fib(5)
/ \
fib(4) fib(3)
/ \ / \
fib(3) fib(2) fib(2) fib(1)
/ \ / \ / \ |
f(2) f(1) f(1) f(0) f(1) f(0) 1
/ \ | | | | |
fib(1) fib(0) 1 1 0 1 0
| |
1 0
Notice: fib(3) is calculated TWICE!
fib(2) is calculated THREE times!
fib(1) is calculated FIVE times!
This is why recursive Fibonacci is so slow.
(We'll learn about better approaches in data structures courses)
2.7 What Can Go Wrong: Stack Overflow
Remember how the stack has limited space? If recursion goes too deep, you run out of stack space and get a stack overflow error.
/*
* File: stack_overflow_demo.c
*
* WARNING: This program will CRASH!
* It demonstrates what happens with infinite recursion.
*
* DO NOT run this expecting useful output.
* It's here to show what NOT to do.
*/
#include <stdio.h>
/*
* Function: infinite_recursion
* ----------------------------
* THIS FUNCTION HAS NO BASE CASE!
* It will call itself forever (until the stack overflows).
*
* Each call creates a new stack frame.
* Eventually, we run out of stack space.
* The program crashes with "Segmentation fault" or "Stack overflow"
*/
int infinite_recursion(int n)
{
printf("Call #%d\n", n);
/*
* DANGER: No base case!
* This function will keep calling itself forever.
*/
return infinite_recursion(n + 1); // Oops! No way to stop!
}
/*
* Function: deep_but_finite
* -------------------------
* This function HAS a base case, but if you call it with
* a very large number, it might still overflow the stack.
*
* Typical stack sizes are 1-8 MB. Each stack frame might be
* 32-64 bytes or more. So you can make roughly 100,000+ calls
* before running out of space (varies by system).
*/
int deep_but_finite(int n)
{
if (n <= 0)
{
return 0; // Base case - stops the recursion
}
return deep_but_finite(n - 1);
}
int main()
{
/*
* This will crash after a few thousand/million calls:
* infinite_recursion(1);
*/
/*
* This might work, or might crash, depending on stack size:
* deep_but_finite(1000000);
*/
printf("This program demonstrates stack overflow concepts.\n");
printf("Uncomment the dangerous calls to see them in action.\n");
printf("(But be prepared for a crash!)\n");
return 0;
}
STACK OVERFLOW VISUALIZATION
═══════════════════════════════════════════
Memory:
┌─────────────────────────────────────────┐ High Address
│ infinite_recursion(9999) │
├─────────────────────────────────────────┤
│ infinite_recursion(9998) │
├─────────────────────────────────────────┤
│ infinite_recursion(9997) │
├─────────────────────────────────────────┤
│ ... thousands more ... │ ← Stack keeps growing
├─────────────────────────────────────────┤
│ infinite_recursion(2) │
├─────────────────────────────────────────┤
│ infinite_recursion(1) │
├─────────────────────────────────────────┤
│ main() │
├─────────────────────────────────────────┤
│ │
│ ██████████ STACK LIMIT ██████████████ │ ← CRASH! No more room!
│ │
├─────────────────────────────────────────┤
│ HEAP │
└─────────────────────────────────────────┘ Low Address
Error message you might see:
╔═══════════════════════════════════════════╗
║ Segmentation fault (core dumped) ║
║ -- or -- ║
║ Stack overflow ║
╚═══════════════════════════════════════════╝
2.8 Key Takeaways from Part 2
1. Recursion = A Function Calling Itself
int factorial(int n)
{
if (n <= 1) return 1; // Base case: STOP!
return n * factorial(n - 1); // Recursive case: call self
}
2. Every Recursive Function Needs a Base Case
Without a base case, recursion continues forever until the stack overflows.
3. Each Recursive Call Gets Its Own Stack Frame
Just like regular function calls, each recursive call has its own:
Parameters
Local variables
Return address
4. Stack Unwinding Happens in Reverse Order
Calls go:
main → f(4) → f(3) → f(2) → f(1)Returns go:
f(1) → f(2) → f(3) → f(4) → main
5. Deep Recursion Can Cause Stack Overflow
The stack has limited space. Very deep recursion (or infinite recursion) will crash your program.
Why This Matters for Pointers
Understanding stack unwinding is crucial for the next section on pointers. Here's a preview of why:
/*
* DANGEROUS CODE - DO NOT DO THIS!
*
* This function returns a pointer to a local variable.
* When the function returns, the local variable is DESTROYED
* (its stack frame is unwound), but the pointer still points
* to that memory location!
*/
int* dangerous_function()
{
int local_var = 42; // Lives on the stack
return &local_var; // Returns address of local_var
} // local_var is DESTROYED here!
// The returned pointer now points to GARBAGE!
We'll explore this danger (and how to avoid it) in detail in Part 3.
Part 3: Pointers Demystified
Pointers are often considered the most challenging concept in C. But here's a secret: pointers are just memory addresses. That's it. Once you truly understand this, everything else falls into place.
In this section, we'll build your understanding from the ground up, connecting what you've learned about the stack to explain both the power and the dangers of pointers.
3.1 What is a Pointer?
Every variable in your program lives somewhere in memory. That "somewhere" has an address - a number that identifies that specific location in memory.
A pointer is simply a variable that stores a memory address.
REGULAR VARIABLE vs POINTER
════════════════════════════════════════════════════════════════
Regular variable 'x': Pointer variable 'ptr':
┌─────────────┐ ┌─────────────┐
│ 42 │ ← stores a value │ 0x1000 │ ← stores an ADDRESS
└─────────────┘ └─────────────┘
Address: 0x1000 Address: 0x2000
The pointer 'ptr' POINTS TO 'x' because it contains x's address:
ptr x
┌─────────┐ ┌─────────┐
│ 0x1000 │ ──────────────────► │ 42 │
└─────────┘ └─────────┘
(contains (lives at
address 0x1000) address 0x1000)
Declaring and Using Pointers
/*
* File: pointer_basics.c
*
* This program demonstrates the fundamentals of pointers:
* - Declaring pointers
* - The address-of operator (&)
* - The dereference operator (*)
*/
#include <stdio.h>
int main()
{
/*
* STEP 1: Create a regular integer variable
*
* This allocates space on the stack for an integer
* and stores the value 42 in that space.
*/
int x = 42;
/*
* STEP 2: Create a pointer to an integer
*
* The asterisk (*) in the declaration means "pointer to".
* So "int *ptr" means "ptr is a pointer to an integer".
*
* We initialize it to NULL (which means "points to nothing")
* for safety. NULL is defined as address 0, which is never
* a valid memory location for your data.
*/
int *ptr = NULL;
/*
* STEP 3: Make the pointer point to x
*
* The ampersand (&) is the "address-of" operator.
* &x means "the memory address where x is stored".
*
* After this line, ptr contains the address of x.
*/
ptr = &x;
/*
* STEP 4: Access the value through the pointer
*
* The asterisk (*) when used with an existing pointer is
* the "dereference" operator. It means "go to the address
* stored in this pointer and get/set the value there".
*
* *ptr means "the value at the address stored in ptr"
* Since ptr contains the address of x, *ptr gives us 42.
*/
printf("Value of x: %d\n", x); // Direct access: 42
printf("Address of x: %p\n", &x); // x's address (hex)
printf("Value of ptr: %p\n", ptr); // Same address!
printf("Value pointed to by ptr: %d\n", *ptr); // 42
/*
* STEP 5: Modify x through the pointer
*
* Since *ptr refers to the same memory location as x,
* changing *ptr also changes x!
*/
*ptr = 100; // This changes x!
printf("\nAfter *ptr = 100:\n");
printf("Value of x: %d\n", x); // Now 100!
printf("Value pointed to by ptr: %d\n", *ptr); // Also 100
return 0;
}
Output:
Value of x: 42
Address of x: 0x7ffd5e8e3abc
Value of ptr: 0x7ffd5e8e3abc
Value pointed to by ptr: 42
After *ptr = 100:
Value of x: 100
Value pointed to by ptr: 100
The Two Meanings of the Asterisk (*)
This is a common source of confusion. The * symbol has TWO different meanings in C:
/*
* MEANING 1: In a DECLARATION, * means "pointer to"
*/
int *ptr; // ptr is a "pointer to int"
char *str; // str is a "pointer to char"
float *fp; // fp is a "pointer to float"
/*
* MEANING 2: In an EXPRESSION, * means "dereference" (go to that address)
*/
int x = 42;
int *ptr = &x; // Declaration: ptr is a pointer, initialized to address of x
int y = *ptr; // Expression: y gets the value AT the address in ptr (42)
*ptr = 100; // Expression: store 100 AT the address in ptr
Memory Visualization
int x = 42;
int *ptr = &x;
MEMORY LAYOUT
══════════════════════════════════════════════════════════
Address Variable Value Notes
──────── ──────── ───── ─────────────────
0x7ffd100 x 42 Regular int variable
0x7ffd108 ptr 0x7ffd100 Pointer variable
(stores ADDRESS of x)
Visual representation:
ptr x
┌──────────────┐ ┌──────────────┐
│ 0x7ffd100 │ ────────────────►│ 42 │
└──────────────┘ └──────────────┘
Address: 0x7ffd108 Address: 0x7ffd100
Reading the diagram:
- ptr is stored at address 0x7ffd108
- ptr CONTAINS the value 0x7ffd100 (which is x's address)
- Following the arrow (dereferencing) leads us to x
- x contains 42
3.2 The Truth About Pass By Value
Here's one of the most important things to understand about C:
Everything in C is pass by value. Always. No exceptions.
When you call a function, the values of arguments are copied into the parameters. The function works with these copies, not the originals.
The Classic Swap Problem
/*
* File: swap_problem.c
*
* This program demonstrates why swapping doesn't work
* without pointers - because C is pass-by-value.
*/
#include <stdio.h>
/*
* Function: swap_broken
* ---------------------
* DOES NOT WORK!
*
* This function receives COPIES of the values.
* Swapping the copies doesn't affect the originals.
*/
void swap_broken(int a, int b)
{
printf(" Inside swap_broken:\n");
printf(" Before swap: a = %d, b = %d\n", a, b);
/*
* These are LOCAL COPIES of the original values.
* We're swapping the copies, not the originals!
*/
int temp = a;
a = b;
b = temp;
printf(" After swap: a = %d, b = %d\n", a, b);
printf(" (But this only swapped the local copies!)\n");
}
// When this function returns, a and b are DESTROYED.
// The original variables in main() are unchanged.
int main()
{
int x = 5;
int y = 10;
printf("Before calling swap_broken:\n");
printf(" x = %d, y = %d\n\n", x, y);
/*
* When we call swap_broken(x, y):
* - The VALUE of x (5) is COPIED to parameter a
* - The VALUE of y (10) is COPIED to parameter b
* - x and y themselves are NOT passed to the function
*/
swap_broken(x, y);
printf("\nAfter calling swap_broken:\n");
printf(" x = %d, y = %d\n", x, y);
printf(" (x and y are UNCHANGED!)\n");
return 0;
}
Output:
Before calling swap_broken:
x = 5, y = 10
Inside swap_broken:
Before swap: a = 5, b = 10
After swap: a = 10, b = 5
(But this only swapped the local copies!)
After calling swap_broken:
x = 5, y = 10
(x and y are UNCHANGED!)
Visualizing Why It Fails
WHEN swap_broken(x, y) IS CALLED:
══════════════════════════════════════════════════════════
STACK FRAME: main() STACK FRAME: swap_broken()
┌─────────────────────┐ ┌─────────────────────┐
│ x = 5 │ ──COPY──► │ a = 5 │
│ y = 10 │ ──COPY──► │ b = 10 │
└─────────────────────┘ │ temp = ? │
└─────────────────────┘
x and y are SEPARATE a and b are COPIES
from a and b! in their own memory!
AFTER THE SWAP (inside swap_broken):
┌─────────────────────┐ ┌─────────────────────┐
│ x = 5 │ │ a = 10 ← swapped │
│ y = 10 │ │ b = 5 ← swapped │
└─────────────────────┘ │ temp = 5 │
↑ └─────────────────────┘
│
UNCHANGED! These get DESTROYED when
the function returns!
3.3 "Pass By Reference" is Really Pass By Value of an Address
To actually swap the values, we need to pass the addresses of x and y. This way, the function can reach back into main()'s stack frame and modify the original variables.
But here's the key insight: we're still passing by value! We're passing the VALUES of the addresses.
/*
* File: swap_working.c
*
* This program demonstrates the correct way to swap values
* using pointers. We pass the ADDRESSES of the variables.
*/
#include <stdio.h>
/*
* Function: swap_working
* ----------------------
* Actually swaps two integers!
*
* Parameters:
* a - pointer to the first integer (we receive the ADDRESS)
* b - pointer to the second integer (we receive the ADDRESS)
*
* By receiving addresses, we can modify the original variables.
*/
void swap_working(int *a, int *b)
{
printf(" Inside swap_working:\n");
printf(" a points to address: %p\n", (void*)a);
printf(" b points to address: %p\n", (void*)b);
printf(" Value at *a: %d, Value at *b: %d\n", *a, *b);
/*
* *a means "the value at the address stored in a"
* *b means "the value at the address stored in b"
*
* By dereferencing, we access the ORIGINAL variables in main()!
*/
int temp = *a; // temp gets the value at address a (x's value)
*a = *b; // Store value at address b INTO address a
*b = temp; // Store temp INTO address b
printf(" After swap: *a = %d, *b = %d\n", *a, *b);
}
int main()
{
int x = 5;
int y = 10;
printf("Before calling swap_working:\n");
printf(" x = %d (at address %p)\n", x, (void*)&x);
printf(" y = %d (at address %p)\n\n", y, (void*)&y);
/*
* We pass &x and &y - the ADDRESSES of x and y.
*
* These addresses are COPIED to parameters a and b.
* So a contains the same address as &x.
* And b contains the same address as &y.
*
* This is STILL pass-by-value! We're passing the VALUES
* of the addresses. But since a and b have the addresses,
* they can reach x and y through dereferencing.
*/
swap_working(&x, &y);
printf("\nAfter calling swap_working:\n");
printf(" x = %d\n", x);
printf(" y = %d\n", y);
printf(" (Successfully swapped!)\n");
return 0;
}
Output:
Before calling swap_working:
x = 5 (at address 0x7ffd5e8e3abc)
y = 10 (at address 0x7ffd5e8e3ac0)
Inside swap_working:
a points to address: 0x7ffd5e8e3abc
b points to address: 0x7ffd5e8e3ac0
Value at *a: 5, Value at *b: 10
After swap: *a = 10, *b = 5
After calling swap_working:
x = 10
y = 5
(Successfully swapped!)
Visualizing Why It Works
WHEN swap_working(&x, &y) IS CALLED:
══════════════════════════════════════════════════════════
STACK FRAME: main() STACK FRAME: swap_working()
┌─────────────────────┐ ┌─────────────────────┐
│ x = 5 │◄────────────│ a = 0x1000 (addr of x)
│ (at addr 0x1000) │ │ │
│ │ │ │
│ y = 10 │◄────────────│ b = 0x1004 (addr of y)
│ (at addr 0x1004) │ │ │
└─────────────────────┘ │ temp = ? │
└─────────────────────┘
a and b contain ADDRESSES that point back to x and y!
THE SWAP OPERATION:
1. temp = *a; // temp = value at address 0x1000 = 5
2. *a = *b; // value at 0x1000 = value at 0x1004
// x becomes 10!
main() swap_working()
┌─────────────────┐ ┌─────────────────┐
│ x = 10 ←CHANGED│◄─────────────│ a = 0x1000 │
│ y = 10 │◄─────────────│ b = 0x1004 │
└─────────────────┘ │ temp = 5 │
└─────────────────┘
3. *b = temp; // value at 0x1004 = 5
// y becomes 5!
main() swap_working()
┌─────────────────┐ ┌─────────────────┐
│ x = 10 │◄─────────────│ a = 0x1000 │
│ y = 5 ←CHANGED│◄─────────────│ b = 0x1004 │
└─────────────────┘ │ temp = 5 │
└─────────────────┘
RESULT: x and y in main() have been swapped!
The Key Insight
╔════════════════════════════════════════════════════════════════╗
║ "PASS BY REFERENCE" IN C IS A LIE! ║
║ ║
║ What people call "pass by reference" is actually: ║
║ "Pass the VALUE of an ADDRESS" ║
║ ║
║ C is ALWAYS pass-by-value. We just sometimes pass ║
║ address values, which lets us indirectly modify things. ║
║ ║
║ swap_broken(x, y) → passes VALUES 5 and 10 ║
║ swap_working(&x, &y) → passes VALUES 0x1000 and 0x1004 ║
║ (which happen to be addresses) ║
╚════════════════════════════════════════════════════════════════╝
3.4 The Danger: Returning Pointers to Local Variables
Now we connect everything we've learned. Remember from Part 2 how stack frames are destroyed when a function returns? This creates a serious danger with pointers.
/*
* File: dangling_pointer.c
*
* WARNING: This program demonstrates DANGEROUS code!
* Never return a pointer to a local variable!
*
* This code has UNDEFINED BEHAVIOR - it might seem to work,
* crash, or produce garbage values unpredictably.
*/
#include <stdio.h>
/*
* Function: get_value_dangerous
* -----------------------------
* DANGEROUS! DO NOT DO THIS!
*
* Returns a pointer to a local variable.
* When this function returns, the local variable is destroyed,
* but the pointer still contains its (now invalid) address.
*/
int* get_value_dangerous()
{
/*
* 'local' is a LOCAL variable - it lives on the stack,
* inside this function's stack frame.
*/
int local = 42;
printf(" Inside function: local = %d (at address %p)\n",
local, (void*)&local);
/*
* DANGER! We're returning the ADDRESS of 'local'.
* But 'local' will be DESTROYED when this function returns!
*/
return &local; // WARNING: returning address of local variable!
} // <-- RIGHT HERE, the stack frame is destroyed!
// 'local' no longer exists!
// The memory at &local is now INVALID!
int main()
{
printf("Demonstrating dangling pointer danger:\n\n");
/*
* ptr receives the address of 'local' from the function.
* But by the time we receive it, 'local' has been destroyed!
* ptr is now a "dangling pointer" - it points to invalid memory.
*/
int *ptr = get_value_dangerous();
printf(" Back in main: ptr = %p\n", (void*)ptr);
/*
* UNDEFINED BEHAVIOR!
*
* Dereferencing ptr (*ptr) tries to read from memory that
* no longer belongs to our variable. Anything could happen:
*
* 1. It might "work" and print 42 (if memory wasn't reused yet)
* 2. It might print garbage (if memory was overwritten)
* 3. It might crash (segmentation fault)
*
* The result is UNPREDICTABLE and may vary between runs!
*/
printf(" Attempting to read *ptr: %d\n", *ptr);
printf(" (This value is UNRELIABLE - undefined behavior!)\n");
/*
* Let's call another function to demonstrate how the
* memory can get overwritten...
*/
printf("\n Calling another function to corrupt the stack...\n");
// This function call may overwrite the old stack memory
printf(" Some other work: %d + %d = %d\n", 100, 200, 100 + 200);
printf(" Attempting to read *ptr again: %d\n", *ptr);
printf(" (The value may have changed - GARBAGE!)\n");
return 0;
}
Possible Output (varies!):
Demonstrating dangling pointer danger:
Inside function: local = 42 (at address 0x7ffd5e8e3a9c)
Back in main: ptr = 0x7ffd5e8e3a9c
Attempting to read *ptr: 42
(This value is UNRELIABLE - undefined behavior!)
Calling another function to corrupt the stack...
Some other work: 100 + 200 = 300
Attempting to read *ptr again: 300
(The value may have changed - GARBAGE!)
Visualizing the Dangling Pointer
STEP 1: Inside get_value_dangerous()
══════════════════════════════════════════════════════════
┌─────────────────────────────────────────┐
│ get_value_dangerous() │ ← Currently executing
├─────────────────────────────────────────┤
│ local = 42 │ ← Valid! Lives here.
│ (at address 0x7ffd100) │
│ │
│ About to return &local (0x7ffd100) │
└─────────────────────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ main() │
├─────────────────────────────────────────┤
│ ptr = ??? (waiting for return value) │
└─────────────────────────────────────────┘
STEP 2: After get_value_dangerous() returns
══════════════════════════════════════════════════════════
The stack frame is DESTROYED! But ptr still has the address!
┌─────────────────────────────────────────┐
│ ░░░░░░░ FREED MEMORY ░░░░░░░░░░░░░░░░ │ ← Frame destroyed!
│ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ │ local no longer exists!
│ ░░░ (address 0x7ffd100 is invalid) ░░░ │ Memory may contain anything!
└─────────────────────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ main() │ ← Back to main
├─────────────────────────────────────────┤
│ ptr = 0x7ffd100 │ ← DANGLING POINTER!
│ │ │ Points to freed memory!
│ └──────────► ??? (garbage) │
└─────────────────────────────────────────┘
STEP 3: After calling another function
══════════════════════════════════════════════════════════
┌─────────────────────────────────────────┐
│ printf() stack frame │ ← NEW function's data
├─────────────────────────────────────────┤ overwrites the old
│ (some internal variables) │ memory location!
│ (address 0x7ffd100 now has new data!) │
└─────────────────────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ main() │
├─────────────────────────────────────────┤
│ ptr = 0x7ffd100 │ ← Still points there!
│ │ │
│ └──────────► (printf's garbage) │ ← Reading this = BAD!
└─────────────────────────────────────────┘
╔═══════════════════════════════════════════════════════════════╗
║ THIS IS UNDEFINED BEHAVIOR! ║
║ ║
║ • The program might crash ║
║ • It might appear to work (but with wrong data) ║
║ • It might work today and crash tomorrow ║
║ • It might work in debug mode and fail in release mode ║
║ ║
║ NEVER RETURN A POINTER TO A LOCAL VARIABLE! ║
╚═══════════════════════════════════════════════════════════════╝
The Compiler Warns You!
Modern compilers will actually warn you about this. When you compile:
$ gcc -Wall dangling_pointer.c -o dangling_pointer
dangling_pointer.c: In function 'get_value_dangerous':
dangling_pointer.c:25:12: warning: function returns address of local variable [-Wreturn-local-addr]
25 | return &local;
| ^~~~~~
Always compile with warnings enabled (-Wall) and pay attention to them!
3.5 Safe Alternatives
How do we safely return data from a function? Here are three approaches:
Option 1: Return the Value (Not a Pointer)
/*
* SAFE: Return the value itself
*
* The value is COPIED to the caller.
* No pointers, no problems!
*/
int get_value_safe()
{
int local = 42;
return local; // Value is copied out - perfectly safe!
}
int main()
{
int result = get_value_safe(); // result gets a COPY of 42
printf("Result: %d\n", result); // Works perfectly!
return 0;
}
Option 2: Let the Caller Provide Storage
/*
* SAFE: Caller provides the storage location
*
* The function writes to memory owned by the caller.
* When the function returns, the data is still valid
* because it lives in the caller's stack frame.
*/
void get_value_into(int *result)
{
/*
* result points to memory in the CALLER'S stack frame.
* Writing to *result modifies the caller's variable.
* That variable survives after this function returns!
*/
*result = 42;
}
int main()
{
int value; // This lives in main's stack frame
get_value_into(&value); // Function writes to our variable
printf("Value: %d\n", value); // Works perfectly!
return 0;
}
Option 3: Allocate on the Heap (Coming in Part 5!)
/*
* SAFE: Allocate memory on the heap
*
* Heap memory persists until explicitly freed.
* It survives function returns!
*
* (We'll cover this in detail in Part 5)
*/
int* get_value_heap()
{
/*
* malloc allocates memory on the HEAP, not the stack.
* This memory is NOT destroyed when the function returns!
* But the CALLER must remember to free() it later!
*/
int *ptr = malloc(sizeof(int));
*ptr = 42;
return ptr; // Safe! Heap memory survives.
}
int main()
{
int *ptr = get_value_heap();
printf("Value: %d\n", *ptr); // Works perfectly!
free(ptr); // Don't forget to free heap memory!
return 0;
}
3.6 Key Takeaways from Part 3
1. A Pointer is Just a Memory Address
int x = 42;
int *ptr = &x; // ptr stores the ADDRESS of x
2. The Two Meanings of *
int *ptr; // DECLARATION: ptr is a "pointer to int"
int y = *ptr; // EXPRESSION: get the value AT the address in ptr
3. & Gets the Address, * Dereferences
int x = 42;
int *ptr = &x; // & gets address of x
int y = *ptr; // * gets value at that address (42)
*ptr = 100; // * lets us modify the value at that address
4. C is ALWAYS Pass By Value
void func(int a); // Receives a COPY of an int
void func(int *a); // Receives a COPY of an address
// (but can use the address to modify the original)
5. NEVER Return a Pointer to a Local Variable
int* bad() { int x = 42; return &x; } // DANGER! x is destroyed!
int good() { int x = 42; return x; } // Safe - returns a copy
Part 4: Program Memory Layout
When your C program runs, the operating system loads it into memory and divides that memory into distinct regions, each with a specific purpose. Understanding this layout is crucial for truly grasping how C works—and for writing safe, efficient code.
4.1 The Big Picture: Memory Segments
When a C program is loaded into memory, it's organized into these segments:
MEMORY LAYOUT OF A RUNNING C PROGRAM
══════════════════════════════════════════════════════════════════
High Address (e.g., 0xFFFFFFFF)
┌──────────────────────────────────────────────────────────────┐
│ │
│ STACK │
│ │
│ • Local variables │
│ • Function parameters │
│ • Return addresses │
│ • Grows DOWNWARD ↓ │
│ │
│ ↓ ↓ ↓ │
├──────────────────────────────────────────────────────────────┤
│ │
│ (free space) │
│ │
│ Stack and Heap grow toward each other │
│ │
├──────────────────────────────────────────────────────────────┤
│ ↑ ↑ ↑ │
│ │
│ HEAP │
│ │
│ • Dynamically allocated memory (malloc, calloc, etc.) │
│ • Grows UPWARD ↑ │
│ • Must be manually freed │
│ │
├──────────────────────────────────────────────────────────────┤
│ │
│ BSS SEGMENT │
│ (Block Started by Symbol) │
│ │
│ • Uninitialized global variables │
│ • Uninitialized static variables │
│ • Automatically initialized to zero │
│ │
├──────────────────────────────────────────────────────────────┤
│ │
│ DATA SEGMENT │
│ │
│ • Initialized global variables │
│ • Initialized static variables │
│ • String literals (often in read-only section) │
│ │
├──────────────────────────────────────────────────────────────┤
│ │
│ TEXT SEGMENT │
│ (Code Segment) │
│ │
│ • Your compiled machine code │
│ • Usually read-only (prevents accidental modification) │
│ • Shared between processes running the same program │
│ │
└──────────────────────────────────────────────────────────────┘
Low Address (e.g., 0x00000000)
4.2 A Complete Example: Where Does Everything Go?
Let's write a program that demonstrates every type of memory storage, then analyze exactly where each piece of data lives.
/*
* File: memory_layout.c
*
* This program demonstrates where different types of data
* are stored in memory. We'll examine:
* - Text segment (code)
* - Data segment (initialized globals/statics)
* - BSS segment (uninitialized globals/statics)
* - Heap (dynamic allocation)
* - Stack (local variables)
*/
#include <stdio.h>
#include <stdlib.h> // For malloc() and free()
/*
* ═══════════════════════════════════════════════════════════════
* GLOBAL VARIABLES
* ═══════════════════════════════════════════════════════════════
*
* Global variables are declared OUTSIDE of any function.
* They exist for the ENTIRE lifetime of the program.
* They can be accessed from ANY function in this file.
*/
/*
* INITIALIZED global variable → DATA SEGMENT
*
* Because we give it an initial value (42), this goes in
* the DATA segment. The value 42 is stored in the executable
* file and loaded into memory when the program starts.
*/
int global_initialized = 42;
/*
* UNINITIALIZED global variable → BSS SEGMENT
*
* Because we don't give it a value, this goes in the BSS
* segment. It's automatically initialized to 0 by the system.
* The executable file doesn't store the value—just records
* that this variable needs space.
*/
int global_uninitialized;
/*
* Constant string literal → DATA SEGMENT (read-only portion)
*
* String literals like "Hello" are stored in a read-only
* part of the data segment. The pointer 'global_string'
* itself is also in the data segment (initialized).
*/
const char *global_string = "Hello from data segment!";
/*
* ═══════════════════════════════════════════════════════════════
* FUNCTIONS (in TEXT segment)
* ═══════════════════════════════════════════════════════════════
*/
/*
* Function: demonstrate_static
* ----------------------------
* Shows how static local variables work.
*
* Static local variables:
* - Live in DATA or BSS segment (not the stack!)
* - Retain their value between function calls
* - Are initialized only ONCE
*/
void demonstrate_static()
{
/*
* STATIC local variable → DATA SEGMENT (if initialized)
*
* Even though this is declared INSIDE a function,
* the 'static' keyword means:
* 1. It's stored in DATA segment, not on the stack
* 2. It persists between function calls
* 3. The initialization (= 0) only happens ONCE
*/
static int call_count = 0;
/*
* Regular local variable → STACK
*
* This is recreated on the stack every time
* the function is called.
*/
int local_var = 100;
call_count++; // This change persists!
local_var++; // This is lost when function returns
printf(" Static call_count: %d (persists between calls)\n", call_count);
printf(" Local local_var: %d (always starts at 100)\n", local_var);
}
/*
* Function: demonstrate_heap
* --------------------------
* Shows dynamic memory allocation on the heap.
*/
void demonstrate_heap()
{
/*
* 'heap_ptr' is a LOCAL variable → STACK
* But it POINTS TO memory on the → HEAP
*
* The pointer itself (8 bytes on 64-bit) is on the stack.
* The memory it points to (4 bytes for an int) is on the heap.
*/
int *heap_ptr = malloc(sizeof(int));
if (heap_ptr == NULL)
{
printf(" malloc failed!\n");
return;
}
*heap_ptr = 999;
printf(" heap_ptr (the pointer) is at address: %p (on STACK)\n",
(void*)&heap_ptr);
printf(" heap_ptr points to address: %p (on HEAP)\n",
(void*)heap_ptr);
printf(" Value stored on heap: %d\n", *heap_ptr);
/*
* IMPORTANT: We must free heap memory!
* Unlike stack memory (automatic), heap memory persists
* until we explicitly free it.
*/
free(heap_ptr);
printf(" Heap memory freed.\n");
}
/*
* Function: print_addresses
* -------------------------
* Prints the memory addresses of various items to show
* where they're located in the memory layout.
*/
void print_addresses()
{
/*
* Local variables → STACK
*/
int stack_var1 = 10;
int stack_var2 = 20;
char stack_array[10] = "Hi";
/*
* Heap allocation
*/
int *heap_var = malloc(sizeof(int));
*heap_var = 30;
printf("\n");
printf("╔══════════════════════════════════════════════════════════════╗\n");
printf("║ MEMORY ADDRESS DEMONSTRATION ║\n");
printf("╠══════════════════════════════════════════════════════════════╣\n");
printf("║ ║\n");
printf("║ TEXT SEGMENT (Code): ║\n");
printf("║ main function: %p ║\n", (void*)main);
printf("║ print_addresses function: %p ║\n", (void*)print_addresses);
printf("║ ║\n");
printf("╠══════════════════════════════════════════════════════════════╣\n");
printf("║ ║\n");
printf("║ DATA SEGMENT (Initialized globals): ║\n");
printf("║ global_initialized: %p ║\n", (void*)&global_initialized);
printf("║ global_string (ptr): %p ║\n", (void*)&global_string);
printf("║ string literal itself: %p ║\n", (void*)global_string);
printf("║ ║\n");
printf("╠══════════════════════════════════════════════════════════════╣\n");
printf("║ ║\n");
printf("║ BSS SEGMENT (Uninitialized globals): ║\n");
printf("║ global_uninitialized: %p ║\n", (void*)&global_uninitialized);
printf("║ (auto-initialized to %d) ║\n", global_uninitialized);
printf("║ ║\n");
printf("╠══════════════════════════════════════════════════════════════╣\n");
printf("║ ║\n");
printf("║ HEAP (Dynamic allocation): ║\n");
printf("║ heap_var points to: %p ║\n", (void*)heap_var);
printf("║ ║\n");
printf("╠══════════════════════════════════════════════════════════════╣\n");
printf("║ ║\n");
printf("║ STACK (Local variables): ║\n");
printf("║ stack_var1: %p ║\n", (void*)&stack_var1);
printf("║ stack_var2: %p ║\n", (void*)&stack_var2);
printf("║ stack_array: %p ║\n", (void*)stack_array);
printf("║ heap_var (the ptr): %p ║\n", (void*)&heap_var);
printf("║ ║\n");
printf("╚══════════════════════════════════════════════════════════════╝\n");
/*
* Show the relative ordering of addresses
*/
printf("\n");
printf("Notice the address patterns:\n");
printf(" - Text segment has LOW addresses (near 0x4...)\n");
printf(" - Data/BSS are above Text\n");
printf(" - Heap addresses are LOWER than Stack\n");
printf(" - Stack has HIGH addresses (near 0x7fff...)\n");
printf(" - Stack variables are close together\n");
printf(" - stack_var2 has LOWER address than stack_var1\n");
printf(" (Stack grows DOWNWARD!)\n");
free(heap_var);
}
/*
* Function: main
* --------------
* The main() function itself is code → TEXT SEGMENT
*/
int main()
{
printf("═══════════════════════════════════════════════════════════════\n");
printf(" EXPLORING C PROGRAM MEMORY LAYOUT\n");
printf("═══════════════════════════════════════════════════════════════\n\n");
/*
* Demonstrate static variables
*/
printf("1. STATIC VARIABLES (stored in DATA segment):\n");
printf(" Calling demonstrate_static() three times:\n\n");
demonstrate_static();
demonstrate_static();
demonstrate_static();
printf("\n");
printf("═══════════════════════════════════════════════════════════════\n\n");
/*
* Demonstrate heap allocation
*/
printf("2. HEAP ALLOCATION:\n\n");
demonstrate_heap();
printf("\n");
printf("═══════════════════════════════════════════════════════════════\n\n");
/*
* Print addresses of everything
*/
printf("3. MEMORY ADDRESSES:\n");
print_addresses();
return 0;
}
Sample Output:
═══════════════════════════════════════════════════════════════
EXPLORING C PROGRAM MEMORY LAYOUT
═══════════════════════════════════════════════════════════════
1. STATIC VARIABLES (stored in DATA segment):
Calling demonstrate_static() three times:
Static call_count: 1 (persists between calls)
Local local_var: 101 (always starts at 100)
Static call_count: 2 (persists between calls)
Local local_var: 101 (always starts at 100)
Static call_count: 3 (persists between calls)
Local local_var: 101 (always starts at 100)
═══════════════════════════════════════════════════════════════
2. HEAP ALLOCATION:
heap_ptr (the pointer) is at address: 0x7ffd4a3b2c48 (on STACK)
heap_ptr points to address: 0x5648a3c012a0 (on HEAP)
Value stored on heap: 999
Heap memory freed.
═══════════════════════════════════════════════════════════════
3. MEMORY ADDRESSES:
╔══════════════════════════════════════════════════════════════╗
║ MEMORY ADDRESS DEMONSTRATION ║
╠══════════════════════════════════════════════════════════════╣
║ ║
║ TEXT SEGMENT (Code): ║
║ main function: 0x5648a2f01289 ║
║ print_addresses function: 0x5648a2f01145 ║
...
4.3 Detailed Breakdown of Each Segment
TEXT Segment (Code Segment)
TEXT SEGMENT
════════════════════════════════════════════════════════════
What's stored here:
┌─────────────────────────────────────────────────────────┐
│ • Your compiled machine code instructions │
│ • Function bodies (main, printf, your functions) │
│ • Usually marked READ-ONLY by the OS │
│ • Shared between multiple instances of same program │
└─────────────────────────────────────────────────────────┘
In our example:
┌─────────────────────────────────────────────────────────┐
│ main() → Machine code for main │
│ demonstrate_static() → Machine code for this function │
│ demonstrate_heap() → Machine code for this function │
│ print_addresses() → Machine code for this function │
└─────────────────────────────────────────────────────────┘
DATA Segment
DATA SEGMENT
════════════════════════════════════════════════════════════
What's stored here:
┌─────────────────────────────────────────────────────────┐
│ • Initialized global variables │
│ • Initialized static variables (even local statics) │
│ • String literals (often in read-only subsection) │
│ • Values are stored in the executable file │
│ • Loaded into memory at program start │
└─────────────────────────────────────────────────────────┘
In our example:
┌─────────────────────────────────────────────────────────┐
│ global_initialized = 42 │
│ global_string = (pointer to "Hello from data...") │
│ "Hello from data segment!" (the string itself) │
│ call_count = 0 (static local in demonstrate_static) │
└─────────────────────────────────────────────────────────┘
BSS Segment
BSS SEGMENT (Block Started by Symbol)
════════════════════════════════════════════════════════════
What's stored here:
┌─────────────────────────────────────────────────────────┐
│ • Uninitialized global variables │
│ • Uninitialized static variables │
│ • NOT stored in executable (saves space!) │
│ • OS initializes all BSS to ZERO at program start │
└─────────────────────────────────────────────────────────┘
In our example:
┌─────────────────────────────────────────────────────────┐
│ global_uninitialized = 0 (automatically!) │
└─────────────────────────────────────────────────────────┘
Why BSS exists:
┌─────────────────────────────────────────────────────────┐
│ If you have: │
│ int big_array[1000000]; // 4 MB of zeros │
│ │
│ DATA segment would store 4 MB of zeros in the │
│ executable file. Wasteful! │
│ │
│ BSS just records "need 4 MB of space" and the OS │
│ allocates zeroed memory at runtime. Executable │
│ stays small! │
└─────────────────────────────────────────────────────────┘
HEAP
HEAP
════════════════════════════════════════════════════════════
What's stored here:
┌─────────────────────────────────────────────────────────┐
│ • Dynamically allocated memory │
│ • Created by malloc(), calloc(), realloc() │
│ • Must be manually freed with free() │
│ • Grows UPWARD toward higher addresses │
│ • Persists until freed (survives function returns!) │
│ • Can cause memory leaks if not freed │
└─────────────────────────────────────────────────────────┘
In our example:
┌─────────────────────────────────────────────────────────┐
│ malloc(sizeof(int)) allocates 4 bytes here │
│ heap_ptr points TO this location │
│ (but heap_ptr itself is on the stack!) │
└─────────────────────────────────────────────────────────┘
STACK
STACK
════════════════════════════════════════════════════════════
What's stored here:
┌─────────────────────────────────────────────────────────┐
│ • Local variables │
│ • Function parameters │
│ • Return addresses │
│ • Grows DOWNWARD toward lower addresses │
│ • Automatically managed (created/destroyed with calls) │
│ • Limited size (typically 1-8 MB) │
└─────────────────────────────────────────────────────────┘
In our example:
┌─────────────────────────────────────────────────────────┐
│ stack_var1, stack_var2, stack_array │
│ heap_ptr (the pointer variable, not what it points to) │
│ Function parameters │
│ Return addresses │
└─────────────────────────────────────────────────────────┘
4.4 Visual Summary: One Variable, Different Lifetimes
COMPARING STORAGE CLASSES
══════════════════════════════════════════════════════════════════
┌──────────────────┬─────────────┬────────────────┬──────────────┐
│ Declaration │ Location │ Lifetime │ Init Value │
├──────────────────┼─────────────┼────────────────┼──────────────┤
│ int global = 5; │ DATA │ Entire program│ 5 │
│ (outside func) │ │ │ │
├──────────────────┼─────────────┼────────────────┼──────────────┤
│ int global; │ BSS │ Entire program│ 0 (auto) │
│ (outside func) │ │ │ │
├──────────────────┼─────────────┼────────────────┼──────────────┤
│ static int s=5; │ DATA │ Entire program│ 5 │
│ (inside func) │ │ │ │
├──────────────────┼─────────────┼────────────────┼──────────────┤
│ static int s; │ BSS │ Entire program│ 0 (auto) │
│ (inside func) │ │ │ │
├──────────────────┼─────────────┼────────────────┼──────────────┤
│ int local; │ STACK │ Function call │ Garbage! │
│ (inside func) │ │ │ │
├──────────────────┼─────────────┼────────────────┼──────────────┤
│ int local = 5; │ STACK │ Function call │ 5 │
│ (inside func) │ │ │ │
├──────────────────┼─────────────┼────────────────┼──────────────┤
│ malloc(size) │ HEAP │ Until free() │ Garbage! │
│ │ │ │ │
├──────────────────┼─────────────┼────────────────┼──────────────┤
│ calloc(n,size) │ HEAP │ Until free() │ 0 (auto) │
│ │ │ │ │
└──────────────────┴─────────────┴────────────────┴──────────────┘
4.5 Stack vs Heap: A Comparison
STACK vs HEAP
══════════════════════════════════════════════════════════════════
STACK HEAP
───── ────
Allocation: Automatic Manual (malloc)
Deallocation: Automatic Manual (free)
(when function returns)
Size limit: Small (1-8 MB typical) Large (limited by RAM)
Speed: Very fast Slower
(just move stack pointer) (must search for space)
Growth: Downward ↓ Upward ↑
Fragmentation: None Possible
Access: Only current function's Any function can access
frame (without pointers) (with pointer)
Memory errors: Stack overflow Memory leaks,
dangling pointers
Use for: Local variables, Large data structures,
small temporary data data that must outlive
function calls
┌───────────────────────────────────────────────────────────────┐
│ RULE OF THUMB: │
│ │
│ • Use STACK for small, short-lived data │
│ • Use HEAP for large data or data that must persist │
│ • Always free() what you malloc()! │
└───────────────────────────────────────────────────────────────┘
4.6 Common Pitfalls Related to Memory Layout
Pitfall 1: Uninitialized Local Variables
/*
* PITFALL: Uninitialized local variables contain GARBAGE!
*
* Unlike global/static variables (which are zeroed),
* local variables on the stack contain whatever garbage
* was left in that memory location.
*/
#include <stdio.h>
int main()
{
int uninitialized; // DANGER: Contains garbage!
/*
* This might print 0, or 12345, or -987654321...
* You have no idea what's in there!
*/
printf("Uninitialized local: %d\n", uninitialized); // UNPREDICTABLE!
int initialized = 0; // SAFE: We know it's 0
printf("Initialized local: %d\n", initialized); // Always 0
return 0;
}
Pitfall 2: Static Variables Retain Values
/*
* GOTCHA: Static local variables keep their value between calls!
*
* This can be useful, but it can also cause subtle bugs
* if you expect the variable to be reset each time.
*/
#include <stdio.h>
int counter()
{
static int count = 0; // Initialized ONCE, persists forever
count++;
return count;
}
int main()
{
printf("%d\n", counter()); // Prints 1
printf("%d\n", counter()); // Prints 2 (not 1!)
printf("%d\n", counter()); // Prints 3
// count is never reset!
return 0;
}
Pitfall 3: String Literals are Read-Only
/*
* DANGER: String literals are in read-only memory!
*
* Trying to modify them causes undefined behavior
* (usually a crash).
*/
#include <stdio.h>
int main()
{
/*
* str points to a string literal in the DATA segment.
* The string is READ-ONLY!
*/
char *str = "Hello";
// str[0] = 'J'; // CRASH! Can't modify read-only memory!
/*
* This is different - it's an ARRAY on the stack,
* initialized with a COPY of the string.
* We can modify it!
*/
char arr[] = "Hello"; // Array on stack, copies the string
arr[0] = 'J'; // OK! arr is on the stack, we own it
printf("%s\n", arr); // Prints "Jello"
return 0;
}
4.7 Key Takeaways from Part 4
1. Memory is Divided into Segments
High Address → STACK (local variables, grows down)
↓
(free space)
↑
HEAP (malloc, grows up)
BSS (uninitialized globals, zeroed)
DATA (initialized globals)
Low Address → TEXT (code, read-only)
2. Know Where Your Data Lives
int global_init = 1; // DATA segment
int global_uninit; // BSS segment (auto-zeroed)
static int static_var; // BSS segment (auto-zeroed)
void func() {
int local; // STACK (garbage until initialized!)
static int s = 0; // DATA segment (persists!)
int *p = malloc(4); // p is on STACK, *p is on HEAP
}
3. Lifetime Depends on Location
TEXT/DATA/BSS: Lives for entire program
STACK: Lives for duration of function call
HEAP: Lives until you call free()
4. Initialize Your Variables!
int x; // Global: automatically 0
static int y; // Static: automatically 0
int z; // Local: GARBAGE! Always initialize!
int *p = malloc(4); // Heap: GARBAGE! (use calloc for zeros)
Part 5: The Heap and Dynamic Memory
In Part 4, we learned that the heap is where dynamically allocated memory lives. Now it's time to master the tools that let us use it: malloc(), calloc(), realloc(), and free(). Understanding these functions is essential for writing C programs that handle data of unknown or varying sizes.
5.1 Why Do We Need Dynamic Memory?
The stack is great, but it has limitations:
/*
* PROBLEM 1: Stack size is fixed at compile time
*
* What if we don't know how much data we need until runtime?
*/
void process_data()
{
int array[1000]; // What if we need more? Or less?
// We're stuck with 1000!
}
/*
* PROBLEM 2: Stack data dies when function returns
*
* What if we need data to outlive the function that created it?
*/
int* create_array()
{
int array[10]; // Lives on stack
return array; // DANGER! array is destroyed when we return!
}
/*
* PROBLEM 3: Stack space is limited (typically 1-8 MB)
*
* What if we need a really large data structure?
*/
void big_data()
{
int huge[10000000]; // 40 MB! Will likely cause stack overflow!
}
The heap solves all these problems:
Allocate exactly as much memory as you need at runtime
Memory persists until you explicitly free it
Much larger space available (limited only by system RAM)
5.2 malloc() - Memory Allocation
malloc() (memory allocate) requests a block of memory from the heap.
void *malloc(size_t size);
Parameter:
size- number of bytes to allocateReturns: Pointer to the allocated memory, or
NULLif allocation failsMemory contents: UNINITIALIZED (contains garbage!)
/*
* File: malloc_basics.c
*
* Demonstrates basic usage of malloc().
*/
#include <stdio.h>
#include <stdlib.h> // Required for malloc() and free()
int main()
{
/*
* STEP 1: Allocate memory for a single integer
*
* sizeof(int) returns the size of an int in bytes (usually 4).
* malloc returns a void*, which we cast to int*.
*
* Note: In C, the cast (int*) is optional but makes intent clear.
* In C++, the cast is required.
*/
int *single_int = (int*) malloc(sizeof(int));
/*
* STEP 2: ALWAYS check if malloc succeeded!
*
* malloc returns NULL if it can't allocate memory
* (e.g., system is out of memory).
* Dereferencing NULL causes a crash!
*/
if (single_int == NULL)
{
printf("malloc failed! Out of memory.\n");
return 1; // Exit with error code
}
/*
* STEP 3: Use the allocated memory
*
* Remember: malloc memory contains GARBAGE until you initialize it!
*/
printf("Before initialization: *single_int = %d (garbage!)\n", *single_int);
*single_int = 42; // Now it has a meaningful value
printf("After initialization: *single_int = %d\n", *single_int);
/*
* STEP 4: Allocate memory for an array of integers
*
* To allocate an array, multiply the element size by the count.
*/
int count = 5;
int *array = (int*) malloc(count * sizeof(int));
if (array == NULL)
{
printf("malloc failed for array!\n");
free(single_int); // Clean up what we already allocated
return 1;
}
/*
* STEP 5: Initialize and use the array
*
* We can use array indexing just like a regular array!
*/
printf("\nArray (before initialization): ");
for (int i = 0; i < count; i++)
{
printf("%d ", array[i]); // Garbage values!
}
printf("(garbage!)\n");
// Initialize the array
for (int i = 0; i < count; i++)
{
array[i] = (i + 1) * 10; // 10, 20, 30, 40, 50
}
printf("Array (after initialization): ");
for (int i = 0; i < count; i++)
{
printf("%d ", array[i]);
}
printf("\n");
/*
* STEP 6: FREE the memory when done!
*
* This is CRITICAL. Unlike stack memory, heap memory
* is NOT automatically freed. You MUST call free().
*/
free(single_int);
free(array);
printf("\nMemory freed successfully.\n");
return 0;
}
Output:
Before initialization: *single_int = 0 (garbage!)
After initialization: *single_int = 42
Array (before initialization): 0 0 1234567 -5678 42 (garbage!)
Array (after initialization): 10 20 30 40 50
Memory freed successfully.
Visualizing malloc()
BEFORE malloc(sizeof(int)):
══════════════════════════════════════════════════════════════
STACK HEAP
┌─────────────────┐ ┌─────────────────┐
│ single_int = ???│ │ (empty) │
└─────────────────┘ └─────────────────┘
AFTER malloc(sizeof(int)):
══════════════════════════════════════════════════════════════
STACK HEAP
┌─────────────────┐ ┌─────────────────┐
│ single_int = │────────────────►│ ???????? │ (4 bytes)
│ 0x5648a3c012a0│ │ (garbage!) │
└─────────────────┘ └─────────────────┘
single_int (the pointer) is on the STACK
The memory it points to is on the HEAP
AFTER *single_int = 42:
══════════════════════════════════════════════════════════════
STACK HEAP
┌─────────────────┐ ┌─────────────────┐
│ single_int = │────────────────►│ 42 │ (4 bytes)
│ 0x5648a3c012a0│ │ │
└─────────────────┘ └─────────────────┘
AFTER free(single_int):
══════════════════════════════════════════════════════════════
STACK HEAP
┌─────────────────┐ ┌─────────────────┐
│ single_int = │───────────────X │ (freed) │
│ 0x5648a3c012a0│ DANGLING! │ (may be reused)│
└─────────────────┘ └─────────────────┘
WARNING: single_int still contains the old address!
But that memory is no longer ours. This is now a "dangling pointer".
Best practice: set pointer to NULL after freeing.
5.3 calloc() - Contiguous Allocation
calloc() is like malloc, but with two key differences:
Takes number of elements AND size of each element separately
Initializes all memory to ZERO
void *calloc(size_t num_elements, size_t element_size);
/*
* File: calloc_demo.c
*
* Demonstrates calloc() and how it differs from malloc().
*/
#include <stdio.h>
#include <stdlib.h>
int main()
{
int count = 5;
/*
* malloc: allocate 5 integers - contains GARBAGE
*/
int *malloc_array = (int*) malloc(count * sizeof(int));
/*
* calloc: allocate 5 integers - initialized to ZERO
*
* Note the two separate arguments:
* - First: number of elements (5)
* - Second: size of each element (sizeof(int))
*/
int *calloc_array = (int*) calloc(count, sizeof(int));
if (malloc_array == NULL || calloc_array == NULL)
{
printf("Allocation failed!\n");
return 1;
}
/*
* Compare the contents WITHOUT initializing
*/
printf("malloc array (uninitialized): ");
for (int i = 0; i < count; i++)
{
printf("%d ", malloc_array[i]); // Garbage!
}
printf("\n");
printf("calloc array (zero-initialized): ");
for (int i = 0; i < count; i++)
{
printf("%d ", calloc_array[i]); // All zeros!
}
printf("\n");
free(malloc_array);
free(calloc_array);
return 0;
}
Output:
malloc array (uninitialized): 0 0 1385472 0 -1293847
calloc array (zero-initialized): 0 0 0 0 0
malloc vs calloc: When to Use Which?
malloc() vs calloc()
══════════════════════════════════════════════════════════════
┌────────────────────┬───────────────────┬───────────────────┐
│ Feature │ malloc() │ calloc() │
├────────────────────┼───────────────────┼───────────────────┤
│ Arguments │ Total bytes │ Count + Size │
│ │ malloc(20) │ calloc(5, 4) │
├────────────────────┼───────────────────┼───────────────────┤
│ Initialization │ None (garbage) │ All zeros │
├────────────────────┼───────────────────┼───────────────────┤
│ Speed │ Slightly faster │ Slightly slower │
│ │ (no zeroing) │ (must zero memory)│
├────────────────────┼───────────────────┼───────────────────┤
│ Use when │ You'll immediately│ You need zeros or │
│ │ overwrite all data│ want safety │
└────────────────────┴───────────────────┴───────────────────┘
╔═══════════════════════════════════════════════════════════╗
║ TIP: When in doubt, use calloc(). ║
║ The zero-initialization prevents bugs from garbage data. ║
║ The tiny speed difference rarely matters. ║
╚═══════════════════════════════════════════════════════════╝
5.4 realloc() - Resize Allocation
What if you allocated memory for 10 items, but now you need space for 20? realloc() lets you resize an existing allocation.
void *realloc(void *ptr, size_t new_size);
Parameters:
ptr- pointer to previously allocated memory (or NULL)new_size- new size in bytes
Returns: Pointer to resized memory (may be different from
ptr!), or NULL on failureContents: Original data is preserved (up to the smaller of old/new size)
/*
* File: realloc_demo.c
*
* Demonstrates realloc() for resizing dynamic arrays.
*/
#include <stdio.h>
#include <stdlib.h>
/*
* Function: print_array
* ---------------------
* Helper to print array contents.
*/
void print_array(int *arr, int size, const char *label)
{
printf("%s: [", label);
for (int i = 0; i < size; i++)
{
printf("%d", arr[i]);
if (i < size - 1) printf(", ");
}
printf("]\n");
}
int main()
{
/*
* STEP 1: Start with a small array
*/
int capacity = 3;
int *array = (int*) malloc(capacity * sizeof(int));
if (array == NULL)
{
printf("Initial malloc failed!\n");
return 1;
}
// Initialize the array
array[0] = 10;
array[1] = 20;
array[2] = 30;
printf("Initial array (capacity %d):\n", capacity);
print_array(array, capacity, " array");
printf(" array address: %p\n\n", (void*)array);
/*
* STEP 2: Grow the array using realloc
*
* IMPORTANT: realloc may return a DIFFERENT address!
* If there isn't enough contiguous space at the current location,
* realloc will:
* 1. Allocate new memory elsewhere
* 2. Copy the old data to the new location
* 3. Free the old memory
* 4. Return the new address
*/
int new_capacity = 6;
/*
* CRITICAL: Use a temporary pointer!
*
* If realloc fails, it returns NULL but does NOT free the original memory.
* If we did: array = realloc(array, new_size);
* and realloc returned NULL, we'd lose our pointer to the original
* memory, causing a memory leak!
*/
int *temp = (int*) realloc(array, new_capacity * sizeof(int));
if (temp == NULL)
{
printf("realloc failed!\n");
free(array); // We still have the original pointer, so we can free it
return 1;
}
array = temp; // Safe to update our pointer now
// Initialize the new elements
array[3] = 40;
array[4] = 50;
array[5] = 60;
printf("After growing to capacity %d:\n", new_capacity);
print_array(array, new_capacity, " array");
printf(" array address: %p", (void*)array);
printf(" (may have changed!)\n\n");
/*
* STEP 3: Shrink the array
*
* realloc can also make an allocation smaller.
* Data beyond the new size is lost!
*/
new_capacity = 2;
temp = (int*) realloc(array, new_capacity * sizeof(int));
if (temp == NULL)
{
printf("realloc (shrink) failed!\n");
free(array);
return 1;
}
array = temp;
printf("After shrinking to capacity %d:\n", new_capacity);
print_array(array, new_capacity, " array");
printf(" (Elements 30, 40, 50, 60 are LOST!)\n\n");
/*
* STEP 4: Special cases of realloc
*/
printf("Special cases:\n");
// realloc(NULL, size) is equivalent to malloc(size)
int *new_array = (int*) realloc(NULL, 3 * sizeof(int));
printf(" realloc(NULL, size) works like malloc()\n");
// realloc(ptr, 0) is equivalent to free(ptr) on some systems
// But behavior is implementation-defined, so avoid it!
// Use free() explicitly instead.
free(new_array);
free(array);
printf("\nAll memory freed.\n");
return 0;
}
Output:
Initial array (capacity 3):
array: [10, 20, 30]
array address: 0x5648a3c012a0
After growing to capacity 6:
array: [10, 20, 30, 40, 50, 60]
array address: 0x5648a3c016d0 (may have changed!)
After shrinking to capacity 2:
array: [10, 20]
(Elements 30, 40, 50, 60 are LOST!)
Special cases:
realloc(NULL, size) works like malloc()
All memory freed.
Why realloc() Might Change Your Pointer
SCENARIO: realloc() needs more space
══════════════════════════════════════════════════════════════
BEFORE realloc (need to grow from 3 to 6 integers):
HEAP:
┌────────────┬────────────────────────┬──────────────────────┐
│ Our array │ Another allocation │ Free space │
│ [10,20,30] │ (in the way!) │ │
│ 12 bytes │ │ │
└────────────┴────────────────────────┴──────────────────────┘
↑
array points here
Problem: We need 24 bytes, but there's only 12 bytes of
contiguous space here. Something else is in the way!
AFTER realloc:
HEAP:
┌────────────┬────────────────────────┬──────────────────────┐
│ (freed) │ Another allocation │ Our array (moved!) │
│ │ │ [10,20,30,?,?,?] │
│ │ │ 24 bytes │
└────────────┴────────────────────────┴──────────────────────┘
↑
array NOW points here
realloc:
1. Found space at a new location
2. Copied [10, 20, 30] to the new location
3. Freed the old memory
4. Returned the NEW address
╔═══════════════════════════════════════════════════════════╗
║ CRITICAL: Always use the pointer returned by realloc! ║
║ Your old pointer may no longer be valid! ║
╚═══════════════════════════════════════════════════════════╝
5.5 free() - Releasing Memory
free() returns memory to the heap so it can be reused.
void free(void *ptr);
/*
* File: free_demo.c
*
* Demonstrates proper use of free() and common mistakes.
*/
#include <stdio.h>
#include <stdlib.h>
int main()
{
/*
* Basic usage: allocate, use, free
*/
int *ptr = (int*) malloc(sizeof(int));
*ptr = 42;
printf("Value: %d\n", *ptr);
free(ptr); // Memory returned to heap
/*
* BEST PRACTICE: Set pointer to NULL after freeing
*
* This prevents "use after free" bugs. If you accidentally
* try to use the pointer, you'll get a clear crash on NULL
* dereference instead of subtle, hard-to-debug corruption.
*/
ptr = NULL;
/*
* It's safe to free NULL
*
* free(NULL) does nothing. This is defined behavior.
* It means you don't need to check for NULL before freeing.
*/
int *null_ptr = NULL;
free(null_ptr); // Safe! Does nothing.
printf("Successfully demonstrated free().\n");
return 0;
}
Common Mistakes with free()
/*
* File: free_mistakes.c
*
* DO NOT RUN THIS CODE - it demonstrates BUGS!
* These are examples of what NOT to do.
*/
#include <stdio.h>
#include <stdlib.h>
/*
* MISTAKE 1: Use after free (Dangling Pointer)
*/
void use_after_free_bug()
{
int *ptr = (int*) malloc(sizeof(int));
*ptr = 42;
free(ptr); // Memory is freed
// BUG! ptr still contains the old address, but we don't own that memory!
printf("%d\n", *ptr); // UNDEFINED BEHAVIOR!
*ptr = 100; // UNDEFINED BEHAVIOR! May corrupt other data!
// FIX: Set ptr = NULL after free, and check before use
}
/*
* MISTAKE 2: Double free
*/
void double_free_bug()
{
int *ptr = (int*) malloc(sizeof(int));
*ptr = 42;
free(ptr); // First free - OK
free(ptr); // Second free - BUG! Crashes or corrupts heap!
// FIX: Set ptr = NULL after free. free(NULL) is safe.
}
/*
* MISTAKE 3: Freeing non-heap memory
*/
void free_stack_bug()
{
int stack_var = 42;
int *ptr = &stack_var;
free(ptr); // BUG! stack_var is not on the heap!
// This will crash or corrupt memory!
// FIX: Only free() what you malloc()/calloc()/realloc()
}
/*
* MISTAKE 4: Memory leak
*/
void memory_leak_bug()
{
int *ptr = (int*) malloc(sizeof(int));
*ptr = 42;
// Oops! We reassign ptr without freeing the old memory
ptr = (int*) malloc(sizeof(int)); // LEAK! Lost the old address!
free(ptr); // Only frees the second allocation
// The first allocation is LEAKED forever!
// FIX: Always free before reassigning:
// free(ptr);
// ptr = (int*) malloc(sizeof(int));
}
/*
* MISTAKE 5: Freeing part of an allocation
*/
void free_offset_bug()
{
int *array = (int*) malloc(5 * sizeof(int));
int *middle = array + 2; // Points to the middle of the array
free(middle); // BUG! Can only free the original pointer!
// This will crash or corrupt the heap!
// FIX: Always free the original pointer:
// free(array);
}
Summary of free() Rules
RULES FOR free()
══════════════════════════════════════════════════════════════
✓ DO:
• Free every malloc/calloc/realloc allocation exactly once
• Set pointer to NULL after freeing
• Check for NULL before dereferencing (not before freeing)
• Free in the reverse order of allocation (when order matters)
✗ DON'T:
• Use memory after freeing (dangling pointer)
• Free the same memory twice (double free)
• Free memory not from malloc/calloc/realloc
• Free a pointer that's been offset from the original
• Lose the pointer before freeing (memory leak)
PATTERN FOR SAFE MEMORY MANAGEMENT:
─────────────────────────────────────
int *ptr = NULL; // Initialize to NULL
ptr = (int*) malloc(sizeof(int)); // Allocate
if (ptr == NULL) { /* handle error */ }
/* use ptr */
free(ptr); // Free
ptr = NULL; // Prevent dangling pointer
5.6 Why Heap Memory Survives Function Returns
Remember from Part 3 that returning a pointer to a local variable is dangerous because the stack frame is destroyed? Heap memory doesn't have this problem!
/*
* File: heap_survives.c
*
* Demonstrates that heap-allocated memory survives function returns.
* This is the SAFE way to return dynamically-created data.
*/
#include <stdio.h>
#include <stdlib.h>
/*
* Function: create_array
* ----------------------
* Creates an array on the HEAP and returns it.
*
* This is SAFE because heap memory persists until free() is called.
* The caller becomes responsible for freeing the memory!
*
* Parameters:
* size - number of elements in the array
*
* Returns:
* Pointer to a newly allocated array, or NULL on failure.
* CALLER MUST FREE THIS MEMORY!
*/
int* create_array(int size)
{
/*
* Allocate memory on the HEAP
*
* Unlike local variables, this memory is NOT part of our stack frame.
* It will NOT be destroyed when this function returns!
*/
int *array = (int*) calloc(size, sizeof(int));
if (array == NULL)
{
return NULL; // Allocation failed
}
// Initialize with some values
for (int i = 0; i < size; i++)
{
array[i] = (i + 1) * 100; // 100, 200, 300, ...
}
printf(" create_array: allocated at address %p\n", (void*)array);
/*
* Return the pointer to heap memory.
* The heap memory survives! Only the local pointer variable 'array'
* is destroyed. But the memory it pointed to remains allocated.
*/
return array;
} // The local variable 'array' is destroyed here,
// but the HEAP MEMORY it pointed to is NOT!
int main()
{
printf("Demonstrating that heap memory survives function returns:\n\n");
/*
* Call the function to create an array
*
* The array was allocated inside create_array(), but it
* still exists after create_array() returns!
*/
int size = 5;
int *my_array = create_array(size);
if (my_array == NULL)
{
printf("Failed to create array!\n");
return 1;
}
printf(" main: received array at address %p\n\n", (void*)my_array);
/*
* We can use the array even though create_array() has returned!
*/
printf("Array contents (created in another function):\n ");
for (int i = 0; i < size; i++)
{
printf("%d ", my_array[i]);
}
printf("\n\n");
/*
* CRITICAL: The caller must free the memory!
*
* Since we received a heap-allocated pointer, it's now
* our responsibility to free it when we're done.
*/
free(my_array);
my_array = NULL;
printf("Memory freed by caller. No memory leak!\n");
return 0;
}
Output:
Demonstrating that heap memory survives function returns:
create_array: allocated at address 0x5648a3c012a0
main: received array at address 0x5648a3c012a0
Array contents (created in another function):
100 200 300 400 500
Memory freed by caller. No memory leak!
Comparing Stack vs Heap Returns
STACK ALLOCATION (DANGEROUS)
══════════════════════════════════════════════════════════════
int* bad_function() {
int local_array[5] = {1,2,3,4,5}; // STACK
return local_array; // DANGER!
}
DURING bad_function():
┌─────────────────────────────────────┐
│ bad_function() stack frame │
│ ┌─────────────────────────────┐ │
│ │ local_array: [1,2,3,4,5] │ │ ← About to return this address
│ └─────────────────────────────┘ │
└─────────────────────────────────────┘
AFTER bad_function() returns:
┌─────────────────────────────────────┐
│ ░░░░░░░ DESTROYED ░░░░░░░░░░░░░░░ │
│ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ │ ← Pointer points HERE (garbage!)
└─────────────────────────────────────┘
HEAP ALLOCATION (SAFE)
══════════════════════════════════════════════════════════════
int* good_function() {
int *heap_array = malloc(5 * sizeof(int)); // HEAP
// ... initialize ...
return heap_array; // SAFE!
}
DURING good_function():
STACK HEAP
┌───────────────────┐ ┌─────────────────────┐
│ good_function() │ │ │
│ ┌───────────────┐ │ │ [1, 2, 3, 4, 5] │
│ │ heap_array ───┼─┼──────────────►│ │
│ └───────────────┘ │ │ │
└───────────────────┘ └─────────────────────┘
AFTER good_function() returns:
STACK HEAP
┌───────────────────┐ ┌─────────────────────┐
│ main() │ │ │
│ ┌───────────────┐ │ │ [1, 2, 3, 4, 5] │
│ │ result ───────┼─┼──────────────►│ STILL HERE! │
│ └───────────────┘ │ │ │
└───────────────────┘ └─────────────────────┘
The stack frame for good_function() is gone,
but the heap memory persists!
5.7 Memory Leaks
A memory leak occurs when you allocate heap memory but never free it. The memory remains allocated but unreachable, wasting resources.
/*
* File: memory_leak_demo.c
*
* Demonstrates memory leaks and how to avoid them.
*/
#include <stdio.h>
#include <stdlib.h>
/*
* MEMORY LEAK EXAMPLE 1: Forgetting to free
*/
void leak_example_1()
{
int *ptr = (int*) malloc(1000 * sizeof(int));
// Use the memory...
ptr[0] = 42;
// Oops! Function ends without calling free(ptr)!
// The memory is LEAKED - it's still allocated but we've
// lost the pointer to it!
} // LEAK! 4000 bytes lost forever (until program ends)
/*
* MEMORY LEAK EXAMPLE 2: Overwriting pointer
*/
void leak_example_2()
{
int *ptr = (int*) malloc(100 * sizeof(int)); // Allocation 1
// Oops! We reassign without freeing first!
ptr = (int*) malloc(200 * sizeof(int)); // Allocation 2
// Allocation 1 is now LEAKED - we lost the pointer!
free(ptr); // Only frees Allocation 2
} // Allocation 1 (400 bytes) leaked!
/*
* MEMORY LEAK EXAMPLE 3: Early return without cleanup
*/
int leak_example_3(int value)
{
int *data = (int*) malloc(100 * sizeof(int));
if (value < 0)
{
return -1; // LEAK! Returned without freeing!
}
// Normal processing...
free(data); // Only reached if value >= 0
return 0;
}
/*
* FIXED VERSION: Proper cleanup on all paths
*/
int no_leak_example(int value)
{
int *data = (int*) malloc(100 * sizeof(int));
if (data == NULL)
{
return -1; // No memory allocated, nothing to free
}
if (value < 0)
{
free(data); // Clean up before early return!
return -1;
}
// Normal processing...
free(data); // Clean up on normal path
return 0;
}
int main()
{
printf("Memory leak demonstration.\n");
printf("In real code, avoid these patterns!\n\n");
// Each call to these functions leaks memory:
// leak_example_1();
// leak_example_2();
// leak_example_3(-5);
// The fixed version doesn't leak:
no_leak_example(-5);
no_leak_example(10);
printf("no_leak_example() properly frees memory on all paths.\n");
return 0;
}
Consequences of Memory Leaks
MEMORY LEAK CONSEQUENCES
══════════════════════════════════════════════════════════════
Short-running programs:
┌─────────────────────────────────────────────────────────┐
│ Memory is reclaimed when program exits. │
│ Leaks are wasteful but may not cause obvious problems. │
└─────────────────────────────────────────────────────────┘
Long-running programs (servers, daemons):
┌─────────────────────────────────────────────────────────┐
│ DANGER! Memory usage grows over time. │
│ │
│ Time: 1 hour Memory: 100 MB │
│ Time: 1 day Memory: 2.4 GB │
│ Time: 1 week Memory: OUT OF MEMORY! CRASH! │
│ │
│ This is why long-running services must be leak-free. │
└─────────────────────────────────────────────────────────┘
FINDING LEAKS:
┌─────────────────────────────────────────────────────────┐
│ Tool: Valgrind (Linux) │
│ Command: valgrind --leak-check=full ./your_program │
│ │
│ Valgrind will report: │
│ - Bytes definitely lost │
│ - Where the leaked memory was allocated │
│ - How much was leaked │
└─────────────────────────────────────────────────────────┘
5.8 Key Takeaways from Part 5
1. The Four Memory Functions
// Allocate (uninitialized)
ptr = malloc(num_bytes);
// Allocate (zero-initialized)
ptr = calloc(count, element_size);
// Resize existing allocation
ptr = realloc(old_ptr, new_size); // May change pointer!
// Free memory
free(ptr);
ptr = NULL; // Best practice!
2. Always Check for NULL
int *ptr = malloc(size);
if (ptr == NULL) {
// Handle error - allocation failed!
}
3. Every malloc Needs a free
// Allocate
int *ptr = malloc(sizeof(int));
// Use...
// Free
free(ptr);
ptr = NULL;
4. Heap Memory Survives Function Returns
int* safe_create() {
int *data = malloc(100); // Lives on heap
return data; // Safe to return!
}
// Caller must free!
5. Common Bugs to Avoid
- Use after free → Set pointer to NULL after freeing
- Double free → Set pointer to NULL after freeing
- Memory leak → Always free what you allocate
- Forgetting NULL check → Always check malloc return value
- Freeing wrong pointer → Only free the original malloc pointer
Part 6: Pointers with Arrays and Structs
In this final part, we'll explore how pointers work with arrays and structures—two of the most common data types in C. You'll understand why arrays are "special" in C, master pointer arithmetic, learn why scanf() needs those ampersands, and discover the elegant -> operator for struct pointers.
6.1 Arrays and Pointers: A Special Relationship
Here's one of C's most important (and confusing) facts:
An array name is essentially a pointer to its first element.
When you use an array name in most expressions, it automatically "decays" into a pointer to the first element.
/*
* File: array_pointer_relationship.c
*
* Demonstrates that arrays and pointers are closely related.
*/
#include <stdio.h>
int main()
{
/*
* Declare an array of 5 integers
*/
int arr[5] = {10, 20, 30, 40, 50};
/*
* The array name 'arr' can be used as a pointer!
*
* arr → address of the first element (same as &arr[0])
* *arr → value of the first element (same as arr[0])
*/
printf("Array basics:\n");
printf(" arr = %p (address of first element)\n", (void*)arr);
printf(" &arr[0] = %p (same thing!)\n", (void*)&arr[0]);
printf(" *arr = %d (value at first element)\n", *arr);
printf(" arr[0] = %d (same thing!)\n\n", arr[0]);
/*
* We can assign the array name to a pointer
*/
int *ptr = arr; // ptr now points to the first element
printf("Pointer assigned from array:\n");
printf(" ptr = %p\n", (void*)ptr);
printf(" *ptr = %d\n", *ptr);
printf(" ptr[0] = %d (yes, you can use [] with pointers!)\n\n", ptr[0]);
/*
* Array indexing and pointer arithmetic are equivalent!
*
* arr[i] is exactly equivalent to *(arr + i)
* ptr[i] is exactly equivalent to *(ptr + i)
*/
printf("Equivalence of arr[i] and *(arr + i):\n");
for (int i = 0; i < 5; i++)
{
printf(" arr[%d] = %d, *(arr + %d) = %d\n",
i, arr[i], i, *(arr + i));
}
return 0;
}
Output:
Array basics:
arr = 0x7ffd5e8e3a90 (address of first element)
&arr[0] = 0x7ffd5e8e3a90 (same thing!)
*arr = 10 (value at first element)
arr[0] = 10 (same thing!)
Pointer assigned from array:
ptr = 0x7ffd5e8e3a90
*ptr = 10
ptr[0] = 10 (yes, you can use [] with pointers!)
Equivalence of arr[i] and *(arr + i):
arr[0] = 10, *(arr + 0) = 10
arr[1] = 20, *(arr + 1) = 20
arr[2] = 30, *(arr + 2) = 30
arr[3] = 40, *(arr + 3) = 40
arr[4] = 50, *(arr + 4) = 50
Memory Layout of an Array
ARRAY IN MEMORY: int arr[5] = {10, 20, 30, 40, 50};
══════════════════════════════════════════════════════════════
Memory Address: 0x100 0x104 0x108 0x10C 0x110
┌────────┬────────┬────────┬────────┬────────┐
Array Elements: │ 10 │ 20 │ 30 │ 40 │ 50 │
└────────┴────────┴────────┴────────┴────────┘
Index: [0] [1] [2] [3] [4]
↑
arr (points here)
&arr[0] (same address)
Key insight: Elements are stored CONTIGUOUSLY in memory.
Each int is 4 bytes, so addresses increase by 4.
╔═══════════════════════════════════════════════════════════╗
║ arr[i] is equivalent to *(arr + i) ║
║ ║
║ The compiler translates array indexing into pointer ║
║ arithmetic behind the scenes! ║
╚═══════════════════════════════════════════════════════════╝
Important Difference: Arrays vs Pointers
Although arrays and pointers are closely related, they're not identical:
/*
* IMPORTANT DIFFERENCES between arrays and pointers
*/
#include <stdio.h>
int main()
{
int arr[5] = {10, 20, 30, 40, 50};
int *ptr = arr;
/*
* DIFFERENCE 1: sizeof
*
* sizeof(arr) gives the total size of the array (20 bytes for 5 ints)
* sizeof(ptr) gives the size of a pointer (8 bytes on 64-bit systems)
*/
printf("sizeof(arr) = %zu bytes (entire array)\n", sizeof(arr));
printf("sizeof(ptr) = %zu bytes (just the pointer)\n", sizeof(ptr));
/*
* DIFFERENCE 2: Assignment
*
* You can reassign a pointer, but you CANNOT reassign an array name.
*/
ptr = ptr + 1; // OK: ptr now points to arr[1]
// arr = arr + 1; // ERROR! Array names are not assignable!
/*
* DIFFERENCE 3: Address-of operator
*
* &arr gives the address of the entire array (same numeric value,
* but different type: int(*)[5] instead of int*)
* &ptr gives the address of the pointer variable itself
*/
printf("\n&arr = %p (address of entire array)\n", (void*)&arr);
printf("&ptr = %p (address of the pointer variable)\n", (void*)&ptr);
return 0;
}
6.2 Pointer Arithmetic
When you add or subtract from a pointer, C automatically scales the operation by the size of the pointed-to type. This makes iterating through arrays natural and intuitive.
/*
* File: pointer_arithmetic.c
*
* Demonstrates how pointer arithmetic works in C.
*/
#include <stdio.h>
int main()
{
/*
* Array of integers (each int is typically 4 bytes)
*/
int numbers[5] = {100, 200, 300, 400, 500};
int *ptr = numbers;
printf("Pointer arithmetic with int* (4 bytes each):\n");
printf("═══════════════════════════════════════════════\n\n");
/*
* When we add 1 to an int*, we move forward by sizeof(int) bytes,
* NOT by 1 byte! This is automatic scaling.
*/
printf("ptr = %p → *ptr = %d\n", (void*)ptr, *ptr);
printf("ptr + 1 = %p → *(ptr+1) = %d\n", (void*)(ptr+1), *(ptr+1));
printf("ptr + 2 = %p → *(ptr+2) = %d\n", (void*)(ptr+2), *(ptr+2));
printf("\nNotice: addresses increase by 4 (sizeof(int)), not 1!\n\n");
/*
* Array of chars (each char is 1 byte)
*/
char letters[5] = {'A', 'B', 'C', 'D', 'E'};
char *cptr = letters;
printf("Pointer arithmetic with char* (1 byte each):\n");
printf("═══════════════════════════════════════════════\n\n");
printf("cptr = %p → *cptr = '%c'\n", (void*)cptr, *cptr);
printf("cptr + 1 = %p → *(cptr+1) = '%c'\n", (void*)(cptr+1), *(cptr+1));
printf("cptr + 2 = %p → *(cptr+2) = '%c'\n", (void*)(cptr+2), *(cptr+2));
printf("\nNotice: addresses increase by 1 (sizeof(char))!\n\n");
/*
* Array of doubles (each double is typically 8 bytes)
*/
double values[3] = {1.1, 2.2, 3.3};
double *dptr = values;
printf("Pointer arithmetic with double* (8 bytes each):\n");
printf("═══════════════════════════════════════════════\n\n");
printf("dptr = %p → *dptr = %.1f\n", (void*)dptr, *dptr);
printf("dptr + 1 = %p → *(dptr+1) = %.1f\n", (void*)(dptr+1), *(dptr+1));
printf("dptr + 2 = %p → *(dptr+2) = %.1f\n", (void*)(dptr+2), *(dptr+2));
printf("\nNotice: addresses increase by 8 (sizeof(double))!\n");
return 0;
}
Output:
Pointer arithmetic with int* (4 bytes each):
═══════════════════════════════════════════════
ptr = 0x7ffd5e8e3a80 → *ptr = 100
ptr + 1 = 0x7ffd5e8e3a84 → *(ptr+1) = 200
ptr + 2 = 0x7ffd5e8e3a88 → *(ptr+2) = 300
Notice: addresses increase by 4 (sizeof(int)), not 1!
Pointer arithmetic with char* (1 byte each):
═══════════════════════════════════════════════
cptr = 0x7ffd5e8e3a7b → *cptr = 'A'
cptr + 1 = 0x7ffd5e8e3a7c → *(cptr+1) = 'B'
cptr + 2 = 0x7ffd5e8e3a7d → *(cptr+2) = 'C'
Notice: addresses increase by 1 (sizeof(char))!
Pointer arithmetic with double* (8 bytes each):
═══════════════════════════════════════════════
dptr = 0x7ffd5e8e3a60 → *dptr = 1.1
dptr + 1 = 0x7ffd5e8e3a68 → *(dptr+1) = 2.2
dptr + 2 = 0x7ffd5e8e3a70 → *(dptr+2) = 3.3
Notice: addresses increase by 8 (sizeof(double))!
Visualizing Pointer Arithmetic
POINTER ARITHMETIC SCALING
══════════════════════════════════════════════════════════════
int arr[4] = {10, 20, 30, 40};
int *ptr = arr;
Memory layout (assuming int = 4 bytes):
Address: 0x100 0x104 0x108 0x10C
┌──────────┬──────────┬──────────┬──────────┐
Value: │ 10 │ 20 │ 30 │ 40 │
└──────────┴──────────┴──────────┴──────────┘
↑ ↑ ↑ ↑
ptr ptr + 1 ptr + 2 ptr + 3
(0x100) (0x104) (0x108) (0x10C)
The formula: (ptr + n) = ptr + (n × sizeof(*ptr))
ptr + 0 = 0x100 + (0 × 4) = 0x100
ptr + 1 = 0x100 + (1 × 4) = 0x104
ptr + 2 = 0x100 + (2 × 4) = 0x108
ptr + 3 = 0x100 + (3 × 4) = 0x10C
╔═══════════════════════════════════════════════════════════╗
║ ptr + n doesn't add n bytes! ║
║ It adds (n × sizeof(element)) bytes. ║
║ This is why pointer arithmetic "just works" for arrays. ║
╚═══════════════════════════════════════════════════════════╝
Iterating Through Arrays with Pointers
/*
* File: pointer_iteration.c
*
* Two equivalent ways to iterate through an array.
*/
#include <stdio.h>
int main()
{
int arr[5] = {10, 20, 30, 40, 50};
int size = 5;
/*
* METHOD 1: Traditional array indexing
*/
printf("Method 1 - Array indexing:\n ");
for (int i = 0; i < size; i++)
{
printf("%d ", arr[i]);
}
printf("\n\n");
/*
* METHOD 2: Pointer arithmetic
*/
printf("Method 2 - Pointer arithmetic:\n ");
for (int *ptr = arr; ptr < arr + size; ptr++)
{
printf("%d ", *ptr);
}
printf("\n\n");
/*
* METHOD 3: Pointer with offset
*/
printf("Method 3 - Pointer with offset:\n ");
int *base = arr;
for (int i = 0; i < size; i++)
{
printf("%d ", *(base + i));
}
printf("\n");
return 0;
}
6.3 Why scanf() Needs Pointers
One of the most common questions from new C programmers: "Why do I need & with scanf()?" The answer lies in pass-by-value.
/*
* File: scanf_explanation.c
*
* Explains why scanf needs pointers (addresses).
*/
#include <stdio.h>
/*
* Imagine you're scanf. You receive a VALUE (copy of x).
* You have no way to modify the original x in main()!
*/
void fake_scanf_broken(int x)
{
x = 42; // This only modifies the local copy!
}
/*
* Now you receive an ADDRESS. You can use it to modify
* the original variable in the caller's stack frame!
*/
void fake_scanf_working(int *x)
{
*x = 42; // This modifies the original through the pointer!
}
int main()
{
int num1 = 0;
int num2 = 0;
/*
* The broken version can't modify our variable
*/
fake_scanf_broken(num1);
printf("After fake_scanf_broken: num1 = %d (unchanged!)\n", num1);
/*
* The working version can modify our variable
*/
fake_scanf_working(&num2);
printf("After fake_scanf_working: num2 = %d (modified!)\n", num2);
/*
* This is exactly why real scanf() needs addresses!
*
* scanf("%d", &num1);
* ↑
* We pass the ADDRESS of num1, so scanf
* can store the input value there.
*/
printf("\nEnter a number: ");
scanf("%d", &num1); // &num1 = address of num1
printf("You entered: %d\n", num1);
return 0;
}
Why Strings Don't Need & with scanf()
/*
* File: scanf_strings.c
*
* Explains why strings don't need & with scanf.
*/
#include <stdio.h>
int main()
{
/*
* For basic types, we need & to get the address:
*/
int num;
scanf("%d", &num); // &num is the address
/*
* For strings (char arrays), the array name IS already an address!
*
* Remember: array names decay to pointers.
* So 'name' is already &name[0].
*/
char name[50];
// These are equivalent:
scanf("%s", name); // name decays to &name[0]
// scanf("%s", &name[0]); // Explicit, but unnecessary
printf("Hello, %s!\n", name);
/*
* HOWEVER, for a single char, you DO need &:
*/
char initial;
scanf(" %c", &initial); // Note the space before %c to skip whitespace
printf("Your initial: %c\n", initial);
return 0;
}
Summary: When to Use & with scanf()
SCANF AND THE & OPERATOR
══════════════════════════════════════════════════════════════
┌─────────────────────┬──────────────────┬───────────────────┐
│ Variable Type │ scanf Usage │ Explanation │
├─────────────────────┼──────────────────┼───────────────────┤
│ int num; │ scanf("%d", &num); │ Need & │
│ float f; │ scanf("%f", &f); │ Need & │
│ double d; │ scanf("%lf", &d); │ Need & │
│ char c; │ scanf("%c", &c); │ Need & │
├─────────────────────┼──────────────────┼───────────────────┤
│ char str[100]; │ scanf("%s", str); │ No & ! │
│ int arr[10]; │ scanf("%d", &arr[i]); │ Need & for│
│ │ │ elements │
└─────────────────────┴──────────────────┴───────────────────┘
Rule: scanf needs an ADDRESS where it can store the input.
• Basic types: Use & to get the address
• Arrays/strings: The name IS already an address (to first element)
• Array elements: Use & because arr[i] is a value, not an address
6.4 Structs and Pointers
Structures (structs) let you group related data together. Pointers to structs are extremely common in C, and there's a special operator (->) to make working with them easier.
/*
* File: struct_basics.c
*
* Introduction to structs and pointers to structs.
*/
#include <stdio.h>
#include <string.h>
/*
* Define a struct to represent a student
*
* A struct is a custom data type that groups related variables.
*/
struct Student
{
char name[50];
int age;
float gpa;
};
int main()
{
/*
* Create a struct variable and initialize it
*/
struct Student alice;
strcpy(alice.name, "Alice"); // Can't use = for strings, use strcpy
alice.age = 20;
alice.gpa = 3.8;
/*
* Access members using the DOT (.) operator
*/
printf("Student information (using dot operator):\n");
printf(" Name: %s\n", alice.name);
printf(" Age: %d\n", alice.age);
printf(" GPA: %.2f\n\n", alice.gpa);
/*
* Create a POINTER to a struct
*/
struct Student *ptr = &alice;
/*
* Access members through pointer - TWO WAYS:
*
* 1. Dereference then dot: (*ptr).member
* 2. Arrow operator: ptr->member
*
* The arrow operator is just convenient syntax!
* ptr->member is EXACTLY equivalent to (*ptr).member
*/
printf("Accessing through pointer:\n");
printf(" Using (*ptr).name: %s\n", (*ptr).name);
printf(" Using ptr->name: %s\n", ptr->name);
printf(" Using ptr->age: %d\n", ptr->age);
printf(" Using ptr->gpa: %.2f\n", ptr->gpa);
/*
* Modify through pointer
*/
ptr->age = 21; // Alice had a birthday!
ptr->gpa = 3.9; // And improved her GPA!
printf("\nAfter modification through pointer:\n");
printf(" alice.age = %d\n", alice.age);
printf(" alice.gpa = %.2f\n", alice.gpa);
return 0;
}
Output:
Student information (using dot operator):
Name: Alice
Age: 20
GPA: 3.80
Accessing through pointer:
Using (*ptr).name: Alice
Using ptr->name: Alice
Using ptr->age: 20
Using ptr->gpa: 3.80
After modification through pointer:
alice.age = 21
alice.gpa = 3.90
The Arrow Operator (->)
THE ARROW OPERATOR: ->
══════════════════════════════════════════════════════════════
When you have a POINTER to a struct, use -> to access members.
struct Student *ptr = &alice;
┌──────────────────────────────────────────────────────────┐
│ ptr->age is equivalent to (*ptr).age │
└──────────────────────────────────────────────────────────┘
Why -> exists:
(*ptr).age requires parentheses because . has higher precedence than *.
Without parentheses, *ptr.age would be parsed as *(ptr.age) - wrong!
The -> operator is cleaner and avoids this confusion.
WHEN TO USE . vs ->
─────────────────────────────────────────────────────────────
struct Student alice; // alice is a struct
struct Student *ptr; // ptr is a pointer to struct
alice.name // Use . with struct variables
ptr->name // Use -> with struct pointers
(*ptr).name // Same as above, but uglier
6.5 Dynamic Allocation of Structs
One of the most common patterns in C is dynamically allocating structs.
/*
* File: dynamic_struct.c
*
* Demonstrates dynamic allocation of structs.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct Book
{
char title[100];
char author[50];
int year;
float price;
};
/*
* Function: create_book
* ---------------------
* Creates a new book on the heap and returns a pointer to it.
*
* CALLER MUST FREE THE RETURNED POINTER!
*/
struct Book* create_book(const char *title, const char *author,
int year, float price)
{
/*
* Allocate memory for one Book struct on the heap
*/
struct Book *book = (struct Book*) malloc(sizeof(struct Book));
if (book == NULL)
{
printf("Memory allocation failed!\n");
return NULL;
}
/*
* Initialize the struct members using -> because book is a pointer
*/
strcpy(book->title, title);
strcpy(book->author, author);
book->year = year;
book->price = price;
return book; // Safe! Heap memory survives function return.
}
/*
* Function: print_book
* --------------------
* Prints book information.
* Takes a pointer to avoid copying the entire struct.
*/
void print_book(const struct Book *book)
{
if (book == NULL)
{
printf("NULL book!\n");
return;
}
printf("Title: %s\n", book->title);
printf("Author: %s\n", book->author);
printf("Year: %d\n", book->year);
printf("Price: $%.2f\n", book->price);
}
int main()
{
printf("Creating books dynamically on the heap:\n\n");
/*
* Create books on the heap
*/
struct Book *book1 = create_book(
"The C Programming Language",
"Kernighan & Ritchie",
1978,
45.99
);
struct Book *book2 = create_book(
"Clean Code",
"Robert C. Martin",
2008,
39.99
);
if (book1 == NULL || book2 == NULL)
{
// Clean up any successful allocations
free(book1);
free(book2);
return 1;
}
/*
* Print book information
*/
printf("Book 1:\n");
print_book(book1);
printf("\nBook 2:\n");
print_book(book2);
/*
* Modify a book through its pointer
*/
book1->price = 49.99; // Price increase!
printf("\nAfter price update:\n");
printf("Book 1 new price: $%.2f\n", book1->price);
/*
* CRITICAL: Free the memory when done!
*/
free(book1);
free(book2);
printf("\nMemory freed. No leaks!\n");
return 0;
}
Output:
Creating books dynamically on the heap:
Book 1:
Title: The C Programming Language
Author: Kernighan & Ritchie
Year: 1978
Price: $45.99
Book 2:
Title: Clean Code
Author: Robert C. Martin
Year: 2008
Price: $39.99
After price update:
Book 1 new price: $49.99
Memory freed. No leaks!
Allocating Arrays of Structs
/*
* File: struct_array.c
*
* Demonstrates allocating an array of structs.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct Point
{
int x;
int y;
};
int main()
{
int num_points = 3;
/*
* Allocate an array of structs
*
* This allocates contiguous memory for 3 Point structs.
*/
struct Point *points = (struct Point*) malloc(
num_points * sizeof(struct Point)
);
if (points == NULL)
{
printf("Allocation failed!\n");
return 1;
}
/*
* Initialize the points
*
* We can use array indexing OR pointer arithmetic:
* points[i] - array-style access
* *(points + i) - pointer arithmetic
* (points + i)->x - pointer to i-th element, then arrow
*/
points[0].x = 10;
points[0].y = 20;
points[1].x = 30;
points[1].y = 40;
// Alternative syntax using pointer arithmetic:
(points + 2)->x = 50;
(points + 2)->y = 60;
/*
* Print all points
*/
printf("Points:\n");
for (int i = 0; i < num_points; i++)
{
printf(" Point %d: (%d, %d)\n", i, points[i].x, points[i].y);
}
/*
* Using a pointer to iterate
*/
printf("\nIterating with pointer:\n");
for (struct Point *p = points; p < points + num_points; p++)
{
printf(" Point at %p: (%d, %d)\n", (void*)p, p->x, p->y);
}
free(points);
return 0;
}
Memory Layout of Struct Array
ARRAY OF STRUCTS IN MEMORY
══════════════════════════════════════════════════════════════
struct Point { int x; int y; };
struct Point *points = malloc(3 * sizeof(struct Point));
Each Point is 8 bytes (two 4-byte ints).
Total allocation: 24 bytes.
Memory:
┌──────────────────┬──────────────────┬──────────────────┐
│ points[0] │ points[1] │ points[2] │
├────────┬─────────┼────────┬─────────┼────────┬─────────┤
│ x: 10 │ y: 20 │ x: 30 │ y: 40 │ x: 50 │ y: 60 │
└────────┴─────────┴────────┴─────────┴────────┴─────────┘
↑ ↑ ↑
points points+1 points+2
(0x100) (0x108) (0x110)
Accessing elements:
─────────────────────────────────────────────────────────────
points[i].x Array indexing, then member access
(points + i)->x Pointer arithmetic, then arrow
(*(points + i)).x Pointer arithmetic, dereference, then dot
All three are equivalent!
6.6 Putting It All Together: A Complete Example
/*
* File: student_database.c
*
* A complete example combining everything we've learned:
* - Dynamic memory allocation
* - Structs and pointers
* - Arrays
* - Pointer arithmetic
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/*
* Student struct definition
*/
struct Student
{
int id;
char name[50];
float grades[3]; // Array inside struct
float average;
};
/*
* Function: calculate_average
* ---------------------------
* Calculates the average of an array of grades.
* Demonstrates pointer arithmetic with arrays.
*/
float calculate_average(float *grades, int count)
{
float sum = 0;
// Using pointer arithmetic to iterate
for (float *ptr = grades; ptr < grades + count; ptr++)
{
sum += *ptr;
}
return sum / count;
}
/*
* Function: create_student
* ------------------------
* Allocates and initializes a student on the heap.
*/
struct Student* create_student(int id, const char *name,
float g1, float g2, float g3)
{
struct Student *s = (struct Student*) malloc(sizeof(struct Student));
if (s == NULL) return NULL;
s->id = id;
strcpy(s->name, name);
s->grades[0] = g1;
s->grades[1] = g2;
s->grades[2] = g3;
s->average = calculate_average(s->grades, 3);
return s;
}
/*
* Function: print_student
* -----------------------
* Prints student information.
*/
void print_student(const struct Student *s)
{
printf("ID: %d\n", s->id);
printf("Name: %s\n", s->name);
printf("Grades: %.1f, %.1f, %.1f\n",
s->grades[0], s->grades[1], s->grades[2]);
printf("Average: %.2f\n", s->average);
}
/*
* Function: find_top_student
* --------------------------
* Finds the student with the highest average.
* Returns a pointer to that student.
*/
struct Student* find_top_student(struct Student **students, int count)
{
if (count == 0 || students == NULL) return NULL;
struct Student *top = students[0];
for (int i = 1; i < count; i++)
{
if (students[i]->average > top->average)
{
top = students[i];
}
}
return top; // Returns pointer to existing student (don't free twice!)
}
int main()
{
printf("╔══════════════════════════════════════════╗\n");
printf("║ STUDENT DATABASE EXAMPLE ║\n");
printf("╚══════════════════════════════════════════╝\n\n");
/*
* Create an array of pointers to students
*
* This is a common pattern: an array where each element
* is a pointer to a dynamically allocated struct.
*/
int num_students = 3;
struct Student **students = (struct Student**) malloc(
num_students * sizeof(struct Student*)
);
if (students == NULL)
{
printf("Failed to allocate student array!\n");
return 1;
}
/*
* Create individual students
*/
students[0] = create_student(101, "Alice", 85.0, 90.0, 88.0);
students[1] = create_student(102, "Bob", 78.0, 82.0, 80.0);
students[2] = create_student(103, "Charlie", 92.0, 95.0, 91.0);
// Check all allocations succeeded
for (int i = 0; i < num_students; i++)
{
if (students[i] == NULL)
{
printf("Failed to create student %d!\n", i);
// Clean up and exit
for (int j = 0; j < i; j++) free(students[j]);
free(students);
return 1;
}
}
/*
* Print all students
*/
printf("All Students:\n");
printf("─────────────────────────────────\n");
for (int i = 0; i < num_students; i++)
{
print_student(students[i]);
printf("\n");
}
/*
* Find and print top student
*/
struct Student *top = find_top_student(students, num_students);
printf("═════════════════════════════════\n");
printf("Top Student: %s (Average: %.2f)\n", top->name, top->average);
printf("═════════════════════════════════\n\n");
/*
* Clean up: free each student, then free the array
*/
for (int i = 0; i < num_students; i++)
{
free(students[i]);
}
free(students);
printf("All memory freed successfully!\n");
return 0;
}
Output:
╔══════════════════════════════════════════╗
║ STUDENT DATABASE EXAMPLE ║
╚══════════════════════════════════════════╝
All Students:
─────────────────────────────────────
ID: 101
Name: Alice
Grades: 85.0, 90.0, 88.0
Average: 87.67
ID: 102
Name: Bob
Grades: 78.0, 82.0, 80.0
Average: 80.00
ID: 103
Name: Charlie
Grades: 92.0, 95.0, 91.0
Average: 92.67
═════════════════════════════════════
Top Student: Charlie (Average: 92.67)
═════════════════════════════════════
All memory freed successfully!
6.7 Key Takeaways from Part 6
1. Arrays Decay to Pointers
int arr[5];
int *ptr = arr; // arr decays to &arr[0]
arr[i] == *(arr + i) // Always equivalent!
2. Pointer Arithmetic is Scaled
int *p; // p + 1 moves by sizeof(int) bytes
char *c; // c + 1 moves by sizeof(char) bytes
double *d; // d + 1 moves by sizeof(double) bytes
3. scanf() Needs Addresses
int x; scanf("%d", &x); // Need & for basic types
char s[50]; scanf("%s", s); // No & for arrays (already address)
4. Arrow Operator for Struct Pointers
struct Student *ptr;
ptr->name // Same as (*ptr).name
ptr->age // Same as (*ptr).age
5. Dynamic Struct Allocation
struct Student *s = malloc(sizeof(struct Student));
s->name = "Alice"; // Use -> with pointer
free(s); // Don't forget to free!