In Part 1, we covered what functions are and how to declare, define, and call them. In this post, we go deep into how values actually travel into and out of functions, what pointers are and how they enable call by reference, where variables “live” (scope), and one of the most important ideas in programming: a function calling itself, known as recursion. This post has been expanded with additional worked examples, memory-level diagrams, and step-by-step traces — the same style of progression used in standard textbooks like Programming in ANSI C by E. Balagurusamy and Let Us C by Yashavant Kanetkar, which are excellent companions to this post for further solved problems and exercises.
How Function Calls Actually Use Memory
Before comparing call by value and call by reference, it helps to see that every variable in your program lives at a specific memory address. You can print a variable’s address using the & (address-of) operator with the %p format specifier:
#include <stdio.h>
int main() {
int x = 10;
printf("Value of x: %d\n", x);
printf("Address of x: %p\n", (void*) &x);
return 0;
}
Sample Output (addresses will differ on your machine)
Value of x: 10
Address of x: 0x7ffee4d3a9ac
Call by Value: The Default in C
When you pass a variable to a function in C, by default, C copies the value of that variable into the function’s parameter — the parameter is a brand-new variable, with its own separate address, that just happens to start out holding the same value. The function works with this private copy — any changes made inside the function do not affect the original variable back in the caller.
#include <stdio.h>
void tryToDouble(int num) {
printf("Address of num (inside function): %p\n", (void*) &num);
num = num * 2;
printf("Inside function, num = %d\n", num);
}
int main() {
int value = 10;
printf("Address of value (inside main): %p\n", (void*) &value);
tryToDouble(value);
printf("Back in main, value = %d\n", value);
return 0;
}
Sample Output (addresses will differ on your machine)
Address of value (inside main): 0x7ffee4d3a9ac
Address of num (inside function): 0x7ffee4d3a988
Inside function, num = 20
Back in main, value = 10
Notice two things: first, num and value have different addresses — they are genuinely two separate memory locations, not the same variable under two names. Second, value in main() is still 10, even though num was doubled inside the function. This is call by value: modifying num only ever affects the memory at num‘s address, which has no connection to value‘s address once the copy has been made.
A Visual Way to Think About It
main()'s memory: tryToDouble()'s memory:
value num
+-------+ +-------+
| 10 | address: 0x...9ac | 10 | address: 0x...988
+-------+ | +-------+
| COPY the VALUE (10) across
+---------------------------->
num becomes 20 here,
but value at 0x...9ac
is never touched.
Call by Reference: Passing Addresses with Pointers
Sometimes you want a function to modify the caller’s actual variable — for example, a function that swaps two numbers, or updates several results at once (since a function can only return one value directly). C achieves this using pointers.
A Quick Pointer Primer
A pointer is a variable that stores a memory address instead of an ordinary value. Two operators are essential here:
&(address-of) — placed before a variable, gives you its address.&xmeans “the address where x is stored.”*(dereference / indirection) — used two different ways depending on context:- In a declaration,
int *p;means “p is a pointer that will hold the address of an int.” - In an expression,
*pmeans “go to the address stored in p, and access the actual value sitting there.”
- In a declaration,
#include <stdio.h>
int main() {
int x = 10;
int *p; // p is a pointer to an int (currently uninitialized — points nowhere valid yet)
p = &x; // p now holds x's address
printf("Value of x directly: %d\n", x);
printf("Value of x via pointer p (*p): %d\n", *p);
printf("Address stored in p: %p\n", (void*) p);
printf("Address of p itself: %p\n", (void*) &p);
*p = 50; // changes the value AT the address p points to — i.e. changes x itself
printf("x after *p = 50: %d\n", x);
return 0;
}
Sample Output
Value of x directly: 10
Value of x via pointer p (*p): 10
Address stored in p: 0x7ffee4d3a9ac
Address of p itself: 0x7ffee4d3a9a0
x after *p = 50: 50
Putting It Together: The swap() Function
#include <stdio.h>
void swap(int *a, int *b) {
printf("Inside swap: a points to address %p, which holds %d\n", (void*) a, *a);
printf("Inside swap: b points to address %p, which holds %d\n", (void*) b, *b);
int temp = *a; // *a means "the value stored at the address a points to"
*a = *b;
*b = temp;
}
int main() {
int x = 5, y = 10;
printf("Before swap: x = %d (at %p), y = %d (at %p)\n", x, (void*) &x, y, (void*) &y);
swap(&x, &y); // passing ADDRESSES, not values
printf("After swap: x = %d, y = %d\n", x, y);
return 0;
}
Sample Output (addresses will differ on your machine)
Before swap: x = 5 (at 0x7ffee4d3a9ac), y = 10 (at 0x7ffee4d3a9a8)
Inside swap: a points to address 0x7ffee4d3a9ac, which holds 5
Inside swap: b points to address 0x7ffee4d3a9a8, which holds 10
After swap: x = 10, y = 5
Breaking This Down, Step by Step
swap(&x, &y)passes the memory addresses ofxandy— not copies of their values.- Inside
swap, the parametersaandbare themselves pointer variables (note their own type:int *a) — they store the addresses that were passed in. *ameans “go to the address stored ina, and read/write the value there.” Sinceaholdsx‘s real address, writing to*awrites directly intox‘s memory.- This is why the swap is visible back in
main()— unlike thetryToDouble()example,swap()never had its own private copy of the values; it always operated on the caller’s original memory through the addresses it was given.
A Practical Use Case: Returning Multiple Values
A function can only return one value. But with pointers, a function can update several variables at once — effectively returning multiple results.
#include <stdio.h>
void minMax(int arr[], int size, int *min, int *max) {
*min = arr[0];
*max = arr[0];
for (int i = 1; i < size; i++) {
if (arr[i] < *min) *min = arr[i];
if (arr[i] > *max) *max = arr[i];
}
}
int main() {
int numbers[] = {23, 5, 67, 12, 89, 1};
int smallest, largest;
minMax(numbers, 6, &smallest, &largest);
printf("Smallest: %d\n", smallest);
printf("Largest: %d\n", largest);
return 0;
}
Sample Output
Smallest: 1
Largest: 89
Note on arrays: unlike ordinary variables, when you pass an array to a function (like numbers above), C automatically passes it as a pointer to its first element — this is often called “array decay.” Arrays are effectively always passed “by reference” in C, even without an explicit &. This is why modifying arr[i] inside a function affects the original array in the caller, and why the array parameter int arr[] in the function signature above is functionally identical to writing int *arr.
Example: A Function That Modifies an Array In Place (Doubling Every Element)
#include <stdio.h>
void doubleAll(int arr[], int size) {
for (int i = 0; i < size; i++) {
arr[i] = arr[i] * 2; // modifies the CALLER's array directly — no pointer syntax (*) needed here
}
}
int main() {
int nums[] = {1, 2, 3, 4, 5};
doubleAll(nums, 5);
printf("After doubling: ");
for (int i = 0; i < 5; i++) {
printf("%d ", nums[i]);
}
printf("\n");
return 0;
}
Sample Output
After doubling: 2 4 6 8 10
Scope: Where Do Variables Live?
Every variable in C has a scope — the region of code where it's valid and accessible — and a lifetime — how long it actually exists in memory. These are related but distinct ideas, and it's worth separating them clearly.
#include <stdio.h>
int globalCounter = 0; // global variable: visible to every function in this file, lives for the whole program
void increment() {
int localValue = 100; // local variable: only exists inside increment()
globalCounter++;
localValue++;
printf("Inside increment: localValue = %d, globalCounter = %d\n", localValue, globalCounter);
}
int main() {
increment();
increment();
increment();
// printf("%d", localValue); // ERROR: localValue doesn't exist here — it's out of scope
printf("Final globalCounter (visible in main too): %d\n", globalCounter);
return 0;
}
Sample Output
Inside increment: localValue = 101, globalCounter = 1
Inside increment: localValue = 101, globalCounter = 2
Inside increment: localValue = 101, globalCounter = 3
Final globalCounter (visible in main too): 3
- Local variables (like
localValue) have local scope (only accessible inside the function they're declared in) and automatic lifetime (created fresh each time the function runs, destroyed when it returns) — that's whylocalValueis always 101 at print time, never accumulating across calls, and whymain()can't access it at all. - Global variables (like
globalCounter) have global (file) scope and lifetime equal to the entire program's run — which is why the increments accumulate across all three calls, and whymain()can read the final value.
Block Scope: A Third, Narrower Level
#include <stdio.h>
int main() {
int x = 5;
if (x > 0) {
int y = 10; // y's scope is limited to this if-block
printf("Inside block: x = %d, y = %d\n", x, y);
}
// printf("%d", y); // ERROR: y is out of scope here, even though we're still in main()
printf("Outside block: x = %d\n", x);
return 0;
}
Any variable declared inside a pair of { } — whether that's a function body, an if block, or a loop body — is only visible within that block. This nested structure (block scope inside function scope inside file/global scope) is one of the most fundamental structural rules in C.
Recursion: A Function That Calls Itself
Recursion is when a function calls itself to solve a smaller version of the same problem, until it reaches a base case simple enough to answer directly.
Starting Simple: Sum of the First N Natural Numbers
Before factorial, it's worth seeing recursion on the simplest possible example: adding up 1 + 2 + 3 + ... + n.
#include <stdio.h>
int sumToN(int n) {
if (n == 0) { // base case
return 0;
}
return n + sumToN(n - 1); // recursive case
}
int main() {
printf("Sum of first 5 natural numbers: %d\n", sumToN(5));
return 0;
}
Sample Output
Sum of first 5 natural numbers: 15
Tracing sumToN(5)
sumToN(5) = 5 + sumToN(4)
= 5 + (4 + sumToN(3))
= 5 + (4 + (3 + sumToN(2)))
= 5 + (4 + (3 + (2 + sumToN(1))))
= 5 + (4 + (3 + (2 + (1 + sumToN(0)))))
= 5 + (4 + (3 + (2 + (1 + 0)))) <- sumToN(0) hits the base case, returns 0
= 15
Example: Factorial
The factorial of n (written n!) is n × (n-1) × (n-2) × ... × 1. Notice that n! = n × (n-1)!, and (n-1)! is just "factorial, but for a smaller number" — the same recursive shape as the sum example above.
#include <stdio.h>
int factorial(int n) {
if (n == 0 || n == 1) { // base case: stops the recursion
return 1;
}
return n * factorial(n - 1); // recursive case
}
int main() {
for (int i = 0; i <= 5; i++) {
printf("%d! = %d\n", i, factorial(i));
}
return 0;
}
Sample Output
0! = 1
1! = 1
2! = 2
3! = 6
4! = 24
5! = 120
What Actually Happens in Memory: The Call Stack
Every function call — recursive or not — gets its own private block of memory called a stack frame (or activation record), holding that call's own copies of its parameters and local variables. These stack frames are stacked on top of each other in the order calls are made, and removed in the reverse order as each call finishes — hence the name call stack.
Calling factorial(4) builds up these stack frames, one on top of the previous:
+-------------------+
| factorial(1) | <- called last, finishes FIRST (base case, returns 1 immediately)
| n = 1 |
+-------------------+
| factorial(2) | <- waiting for factorial(1) to return before it can compute 2 * 1
| n = 2 |
+-------------------+
| factorial(3) | <- waiting for factorial(2) to return
| n = 3 |
+-------------------+
| factorial(4) | <- called FIRST, finishes LAST
| n = 4 |
+-------------------+
| main() |
+-------------------+
Unwinding (as each call returns, its frame is removed from the stack):
factorial(1) returns 1 -> factorial(2) computes 2 * 1 = 2, returns 2
factorial(2) returns 2 -> factorial(3) computes 3 * 2 = 6, returns 6
factorial(3) returns 6 -> factorial(4) computes 4 * 6 = 24, returns 24
factorial(4) returns 24 -> main() receives 24
Every Correct Recursive Function Needs Two Things
- A base case — a condition simple enough to answer without recursing further (here,
n == 0 || n == 1). - A recursive case that moves toward the base case — each call must work on a "smaller" version of the problem (here,
n - 1), guaranteeing it eventually reaches the base case.
Example: Fibonacci Sequence
#include <stdio.h>
int fibonacci(int n) {
if (n == 0) return 0; // base case 1
if (n == 1) return 1; // base case 2
return fibonacci(n - 1) + fibonacci(n - 2); // recursive case
}
int main() {
printf("First 10 Fibonacci numbers:\n");
for (int i = 0; i < 10; i++) {
printf("%d ", fibonacci(i));
}
printf("\n");
return 0;
}
Sample Output
First 10 Fibonacci numbers:
0 1 1 2 3 5 8 13 21 34
Each Fibonacci call branches into two further recursive calls instead of one (fibonacci(n-1) AND fibonacci(n-2)), unlike factorial's single chain — which is why this version becomes noticeably slow for larger n (it recomputes the same values repeatedly). This trade-off between recursion's elegance and its efficiency is explored in much more depth, including a full recursion-tree diagram, in this blog's dedicated post on recursion trees.
Another Classic Example: Greatest Common Divisor (Euclidean Algorithm)
The GCD of two numbers can be computed recursively using the rule: gcd(a, b) = gcd(b, a % b), until b becomes 0, at which point a is the answer.
#include <stdio.h>
int gcd(int a, int b) {
if (b == 0) { // base case
return a;
}
return gcd(b, a % b); // recursive case
}
int main() {
int x = 48, y = 18;
printf("GCD of %d and %d is %d\n", x, y, gcd(x, y));
return 0;
}
Sample Output
GCD of 48 and 18 is 18
Tracing gcd(48, 18)
gcd(48, 18) -> b is not 0, call gcd(18, 48 % 18) = gcd(18, 12)
gcd(18, 12) -> b is not 0, call gcd(12, 18 % 12) = gcd(12, 6)
gcd(12, 6) -> b is not 0, call gcd(6, 12 % 6) = gcd(6, 0)
gcd(6, 0) -> b IS 0, base case reached, return a = 6
So gcd(48, 18) = 6
Recursion vs. Iteration
Anything recursion can do, a loop can also do (and vice versa) — they're different tools for expressing the same kind of repetition. As a rule of thumb:
- Use iteration (loops) when the problem is naturally a simple repeated count or accumulation — usually faster and uses less memory, since it doesn't build up a chain of stack frames.
- Use recursion when the problem is naturally defined in terms of smaller versions of itself (factorial, Fibonacci, GCD, tree/directory traversal, divide-and-conquer algorithms like merge sort) — often much shorter and easier to read than the equivalent loop-based version.
Practice Problems
To test your understanding of this post, try writing these yourself before checking a solution:
- Write a recursive function
power(base, exponent)that computes base raised to exponent, without usingpow()frommath.h. - Write a function using call by reference that takes three
intpointers and sorts the three values into ascending order. - Write a recursive function to reverse the digits of a number (compare this to the
do-whileloop version covered elsewhere on this blog — same problem, different technique). - Write a recursive function to check whether a string is a palindrome.
Summary
- Every variable has both a value and an address — call by value copies the value; call by reference (via pointers) shares the address.
- A pointer stores an address;
&gets a variable's address,*dereferences a pointer to access/modify the value it points to. - Call by value (the default): the function gets a copy; changes inside don't affect the caller's original variable.
- Call by reference (via pointers): the function gets the actual address and can modify the caller's variable directly — also how a function can effectively "return" more than one value.
- Arrays are always passed by reference in C automatically ("array decay") — the pointer itself is copied (call by value), but it still points to the same shared data.
- Local variables exist only during a function's execution (and only within their block); global variables exist for the whole program and are visible everywhere in the file.
- Recursion requires a base case (to stop) and a recursive case that moves toward it (to make progress) — each call gets its own private stack frame, which is why recursive calls don't interfere with each other's variables.
Coming up in Part 3: storage classes (static, extern, auto, register) and what they mean for functions and variables, function pointers, and how to split a program across multiple files using header files.