Functions & Parameter Passing
Description
A function is a self-contained block of code that performs a specific task. Functions allow you to break a large program into smaller, reusable pieces. Instead of writing the same logic multiple times, you write it once inside a function and call it whenever you need it.
Every function has four key components:
- Name — A descriptive identifier for the function (e.g.,
calculateSum,isEven). - Parameters — Input values the function receives from the caller. These act as local variables inside the function.
- Body — The block of code that executes when the function is called.
- Return Value — The result the function sends back to the caller (some functions return nothing, called
voidin C++/Java).
When you call a function and provide arguments, the values are passed to the function's parameters. The mechanism by which this passing happens has a critical impact on whether the function can modify the original data. There are two fundamental strategies:
- Pass by Value: The function receives a copy of the argument. Any modifications inside the function affect only the copy — the original variable in the caller remains unchanged.
- Pass by Reference: The function receives a reference (or address) to the original variable. Any modifications inside the function directly change the original variable in the caller's scope.
Understanding the difference between these two strategies is essential for writing correct programs, avoiding subtle bugs, and knowing when a function's side effects will reach the caller.
Language-specific notes:
- C technically only supports pass by value, but simulates pass by reference using pointers.
- C++ supports both pass by value and true pass by reference using the
&syntax. - Java passes primitives (int, double, etc.) by value and object references by value (sometimes called "pass by sharing").
- Python uses "pass by object reference" (pass by sharing) — reassigning a parameter inside the function does not affect the caller, but mutating a mutable object does.
Examples
Example 1
Task: Write a function that takes two integers and returns their sum.
Input: a = 3, b = 7
Output:
10
Explanation: The function add(a, b) receives copies of 3 and 7 as its parameters. Inside the function, it computes 3 + 7 = 10 and returns the result. The original variables a and b in the caller are not modified — they remain 3 and 7. This is pass by value in action.
Example 2
Task: Write a function that swaps two integers using pass by value.
Input: x = 5, y = 10
Output (after calling swap):
x = 5, y = 10 (unchanged!)
Explanation: When we pass x and y by value, the swap function only swaps the local copies. The caller's x and y remain unchanged. This demonstrates why pass by value cannot be used to write a working swap function — the modifications are lost when the function returns.
Example 3
Task: Write a function that swaps two integers using pass by reference.
Input: x = 5, y = 10
Output (after calling swap):
x = 10, y = 5 (swapped!)
Explanation: When we pass x and y by reference, the function receives direct access to the original variables. Swapping inside the function modifies the actual x and y in the caller. After the function returns, x is 10 and y is 5. This is the classic use case for pass by reference — when you need the function to modify the caller's data.
Constraints
- Functions are fundamental to all programming languages
- Pass by value creates an independent copy of the argument — changes inside the function do not propagate back
- Pass by reference gives the function direct access to the original variable — changes propagate back to the caller
- In C, pass by reference is simulated using pointers (pass the address with
&, receive with*) - In C++, true pass by reference uses the
&in the parameter declaration - In Java, primitive types (int, double, char, boolean) are always passed by value; objects are passed by sharing (the reference is copied, but the object it points to is shared)
- In Python, everything is an object; parameters are passed by object reference (reassignment rebinds locally, mutation affects the original)
Editorial
Function Basics — Defining and Calling Functions
Intuition
Think of a function like a recipe card in a kitchen. The recipe has a name ("Chocolate Cake"), a list of ingredients it needs (parameters), a set of instructions (body), and a final product it produces (return value). When you want a cake, you do not write the entire recipe from scratch every time — you just call "make Chocolate Cake with these ingredients" and get the result.
In programming, functions work the same way:
- You define the function once (write the recipe)
- You call the function whenever you need it (follow the recipe)
- You pass arguments to it (provide the ingredients)
- You receive a return value (get the cake)
The power of functions comes from abstraction — you do not need to know the internal details to use them. You just need to know: what goes in (parameters), what comes out (return value), and what it does (purpose).
Step-by-Step Explanation
Let's trace through a simple function call: add(3, 7) returns 10.
Step 1: The caller has variables a = 3 and b = 7. It invokes add(a, b).
Step 2: The function add is defined with parameters x and y. When called, x receives the value 3 and y receives the value 7.
Step 3: Inside the function body, compute result = x + y = 3 + 7 = 10.
Step 4: The function returns 10 to the caller.
Step 5: The caller receives 10 and stores or prints it.
Step 6: The function's local variables (x, y, result) are destroyed — they exist only during the function call.
Function Call — add(3, 7) — Watch how arguments are copied into parameters, the function body executes, and the return value is sent back to the caller.
Algorithm
- Define a function with a name, parameter list, and return type
- When the function is called, copy argument values into the parameters
- Execute the function body using the local parameter copies
- Return the computed result to the caller
- Local variables inside the function are destroyed after the call completes
Code
#include <iostream>
using namespace std;
// Function definition: takes two ints, returns their sum
int add(int x, int y) {
int result = x + y; // Computation happens with local copies
return result; // Send result back to caller
}
int main() {
int a = 3, b = 7;
// Function call: a and b are copied into x and y
int sum = add(a, b);
cout << "Sum: " << sum << endl; // Output: Sum: 10
cout << "a: " << a << ", b: " << b << endl; // Output: a: 3, b: 7 (unchanged)
return 0;
}def add(x: int, y: int) -> int:
"""Takes two integers and returns their sum."""
result = x + y # Computation with local copies
return result # Send result back to caller
a = 3
b = 7
# Function call: a and b are passed by value (for immutable types)
total = add(a, b)
print(f"Sum: {total}") # Output: Sum: 10
print(f"a: {a}, b: {b}") # Output: a: 3, b: 7 (unchanged)public class FunctionBasics {
// Function definition: takes two ints, returns their sum
static int add(int x, int y) {
int result = x + y; // Computation with local copies
return result; // Send result back to caller
}
public static void main(String[] args) {
int a = 3, b = 7;
// Function call: a and b are copied into x and y
int sum = add(a, b);
System.out.println("Sum: " + sum); // Output: Sum: 10
System.out.println("a: " + a + ", b: " + b); // Output: a: 3, b: 7 (unchanged)
}
}Complexity Analysis
Time Complexity: O(1)
The add function performs a single addition operation and a return — both constant time. The function call overhead (copying two integers, pushing/popping the stack frame) is also constant.
Space Complexity: O(1)
Each function call uses a small, fixed amount of stack space for its parameters and local variables. This does not grow with input size.
Pass by Value — The Copy Mechanism
Intuition
In pass by value, the function receives a photocopy of the original document. You can scribble all over the photocopy — cross out words, add notes, tear it up — and the original document in the filing cabinet remains pristine.
This means:
- The function gets its own independent copy of each argument
- Any modifications to parameters inside the function are local — they vanish when the function returns
- The caller's variables are completely protected from side effects
This is the default mechanism in C, C++, and Java (for primitive types). It is safe and predictable, but it has a limitation: the function cannot communicate changes back to the caller through its parameters. The only way to send information back is through the return value.
The classic demonstration of this limitation is the swap problem: if you try to swap two variables by value, only the local copies swap — the originals remain unchanged.
Step-by-Step Explanation
Let's trace a failed swap using pass by value with x = 5, y = 10:
Step 1: Caller has x = 5, y = 10. It calls swap(x, y).
Step 2: Function receives copies: a = 5, b = 10. These are independent of x and y.
Step 3: Inside swap: temp = a = 5.
Step 4: a = b = 10. (Only the LOCAL copy a changes.)
Step 5: b = temp = 5. (Only the LOCAL copy b changes.)
Step 6: Function returns. Local variables a, b, temp are destroyed.
Step 7: Back in caller: x is still 5, y is still 10. The swap had no effect on the originals!
Failed Swap — Pass by Value — Watch how pass by value creates independent copies. The swap succeeds inside the function but the caller's variables remain unchanged.
Algorithm
Pass by Value mechanism:
- When a function is called, each argument value is COPIED into the corresponding parameter
- The function works with these independent copies
- Any modifications to parameters affect only the copies
- When the function returns, the copies are destroyed
- The caller's original variables are never modified
Consequence: A swap function using pass by value cannot work — the swap happens only on copies.
Code
#include <iostream>
using namespace std;
// Swap using pass by value — DOES NOT WORK!
void swapByValue(int a, int b) {
int temp = a; // Save copy of a
a = b; // Only local copy changes
b = temp; // Only local copy changes
cout << "Inside function: a=" << a << ", b=" << b << endl;
// Prints: a=10, b=5 (swapped locally)
}
int main() {
int x = 5, y = 10;
cout << "Before swap: x=" << x << ", y=" << y << endl;
// Prints: x=5, y=10
swapByValue(x, y); // Passes COPIES of x and y
cout << "After swap: x=" << x << ", y=" << y << endl;
// Prints: x=5, y=10 (UNCHANGED!)
return 0;
}def swap_by_value(a: int, b: int) -> None:
"""Attempt to swap using pass by value — DOES NOT WORK!"""
temp = a # Save copy of a
a = b # Only local name rebinding
b = temp # Only local name rebinding
print(f"Inside function: a={a}, b={b}")
# Prints: a=10, b=5 (swapped locally)
x = 5
y = 10
print(f"Before swap: x={x}, y={y}")
# Prints: x=5, y=10
swap_by_value(x, y) # Integers are immutable — behaves like pass by value
print(f"After swap: x={x}, y={y}")
# Prints: x=5, y=10 (UNCHANGED!)
# In Python, the idiomatic way to swap is: x, y = y, xpublic class SwapByValue {
// Swap using pass by value — DOES NOT WORK!
static void swapByValue(int a, int b) {
int temp = a; // Save copy of a
a = b; // Only local copy changes
b = temp; // Only local copy changes
System.out.println("Inside function: a=" + a + ", b=" + b);
// Prints: a=10, b=5 (swapped locally)
}
public static void main(String[] args) {
int x = 5, y = 10;
System.out.println("Before swap: x=" + x + ", y=" + y);
// Prints: x=5, y=10
swapByValue(x, y); // Passes COPIES of x and y
System.out.println("After swap: x=" + x + ", y=" + y);
// Prints: x=5, y=10 (UNCHANGED! Java primitives are always pass by value)
}
}Complexity Analysis
Time Complexity: O(1)
Copying two integer values and performing three assignments are all constant-time operations.
Space Complexity: O(1)
The function uses three local variables (a, b, temp) on the stack. This is a fixed, constant amount of space regardless of the values being swapped.
Why Pass by Value Fails for Modifications
As demonstrated, pass by value creates independent copies of the arguments. The function operates on these copies in its own private stack frame. When the function returns, the copies are destroyed and all modifications are lost.
This is a fundamental limitation when we need a function to modify the caller's data. Common scenarios where pass by value is insufficient:
- Swapping two variables — the swap only happens on copies
- Updating multiple values — a function can only return one value, but sometimes we need to modify several variables
- Modifying large data structures — copying an entire array or object is expensive both in time and memory
The key insight: We need a mechanism that gives the function access to the original variables, not copies. This is exactly what pass by reference provides — instead of copying the data, we give the function the address (or reference) of the original variable.
Pass by Reference — Direct Access to the Original
Intuition
In pass by reference, instead of giving the function a photocopy of the document, you give it the key to the filing cabinet where the original document is stored. Now the function can open the cabinet, read the original, and even modify it. When the function is done, any changes it made are permanent — they are in the original document.
In practical terms:
- The function receives the memory address (in C) or a reference (in C++) of the original variable
- Through this address/reference, the function can read and write the original variable directly
- Changes made inside the function persist after the function returns
- No copy is made — this is also more memory-efficient for large objects
This is why pass by reference is essential for:
- Swap functions — you need to modify two variables in the caller
- Multiple return values — modify parameters to "return" several results
- Efficiency — avoid copying large structures
In C, pass by reference is achieved using pointers (* and &). In C++, you can use either pointers or the cleaner reference syntax (& in the parameter type).
Step-by-Step Explanation
Let's trace a successful swap using pass by reference with x = 5, y = 10:
Step 1: Caller has x = 5 at memory address 0x100, y = 10 at address 0x104. It calls swap(&x, &y) — passing the addresses.
Step 2: Function receives pointers: a points to 0x100 (where x lives), b points to 0x104 (where y lives). No copies of 5 or 10 are made.
Step 3: temp = *a = value at address 0x100 = 5. Save x's value.
Step 4: *a = *b → write value at 0x104 (which is 10) into 0x100. Now x = 10.
Step 5: *b = temp → write 5 into 0x104. Now y = 5.
Step 6: Function returns. The pointers a, b are destroyed, but x and y in the caller have been directly modified.
Step 7: Back in caller: x = 10, y = 5. The swap succeeded!
Successful Swap — Pass by Reference — Watch how pass by reference gives the function direct access to the caller's variables. The swap modifies the originals, not copies.
Algorithm
Pass by Reference mechanism:
- When a function is called, the ADDRESS (or reference) of each argument is passed — not a copy of the value
- The function accesses the original variable through this address
- Any reads or writes through the reference operate on the original variable
- When the function returns, the references are destroyed, but changes to the originals persist
- The caller sees all modifications
Swap algorithm (pass by reference):
- Save *a in temp
- Copy *b into *a
- Copy temp into *b
Code
#include <iostream>
using namespace std;
// Method 1: Pass by reference using C++ references (cleaner syntax)
void swapByRef(int &a, int &b) {
int temp = a; // a IS x (not a copy)
a = b; // Directly modifies x
b = temp; // Directly modifies y
}
// Method 2: Pass by reference using pointers (C-style)
void swapByPtr(int *a, int *b) {
int temp = *a; // Read value at address a
*a = *b; // Write value at address b into address a
*b = temp; // Write saved value into address b
}
int main() {
int x = 5, y = 10;
// Using C++ references
swapByRef(x, y); // x and y are passed by reference
cout << "After swapByRef: x=" << x << ", y=" << y << endl;
// Output: x=10, y=5 (swapped!)
// Reset
x = 5; y = 10;
// Using pointers
swapByPtr(&x, &y); // Pass addresses of x and y
cout << "After swapByPtr: x=" << x << ", y=" << y << endl;
// Output: x=10, y=5 (swapped!)
return 0;
}# Python does not have true pass by reference for immutable types (int, str, etc.)
# However, we can achieve similar effects with MUTABLE types (lists, dicts)
# Approach 1: Use a list to hold values (simulates pass by reference)
def swap_using_list(arr: list, i: int, j: int) -> None:
"""Swaps elements at indices i and j in the list.
Lists are mutable, so changes persist in the caller."""
arr[i], arr[j] = arr[j], arr[i]
values = [5, 10]
print(f"Before swap: {values}") # [5, 10]
swap_using_list(values, 0, 1)
print(f"After swap: {values}") # [10, 5] — swapped!
# Approach 2: The Pythonic way — tuple unpacking (no function needed)
x, y = 5, 10
print(f"Before: x={x}, y={y}") # x=5, y=10
x, y = y, x # Pythonic swap
print(f"After: x={x}, y={y}") # x=10, y=5
# Approach 3: Return swapped values (functional style)
def swap_functional(a: int, b: int) -> tuple:
return b, a
x, y = 5, 10
x, y = swap_functional(x, y)
print(f"After functional swap: x={x}, y={y}") # x=10, y=5public class SwapByReference {
// Java does NOT support pass by reference for primitives.
// To modify caller's data, we use wrapper objects or arrays.
// Approach 1: Use an array (elements are mutable via the reference)
static void swapArray(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
// Approach 2: Use a wrapper class
static class IntWrapper {
int value;
IntWrapper(int value) { this.value = value; }
}
static void swapWrappers(IntWrapper a, IntWrapper b) {
int temp = a.value;
a.value = b.value;
b.value = temp;
}
public static void main(String[] args) {
// Array approach
int[] values = {5, 10};
System.out.println("Before: " + values[0] + ", " + values[1]);
swapArray(values, 0, 1);
System.out.println("After: " + values[0] + ", " + values[1]);
// Output: After: 10, 5
// Wrapper approach
IntWrapper x = new IntWrapper(5);
IntWrapper y = new IntWrapper(10);
swapWrappers(x, y);
System.out.println("After wrapper swap: " + x.value + ", " + y.value);
// Output: After wrapper swap: 10, 5
}
}Complexity Analysis
Time Complexity: O(1)
The swap operation performs three assignments and no loops. Passing an address/reference is also a constant-time operation.
Space Complexity: O(1)
Pass by reference is actually MORE space-efficient than pass by value for large objects. Instead of copying the entire object, we only pass a small address (typically 4 or 8 bytes). For the swap function, we use one extra variable (temp) regardless of the data size.
Summary — When to Use Each Approach
Intuition
Now that we understand both mechanisms, here is a clear decision framework:
Use Pass by Value when:
- You want to protect the original data from accidental modification
- The data is small (integers, characters, booleans) and copying is cheap
- The function only needs to READ the data, not modify it
- You want pure functions with no side effects (easier to reason about, test, and debug)
Use Pass by Reference when:
- You need the function to modify the caller's variables
- You want to avoid copying large objects (arrays, strings, structures) for performance
- You need to return multiple values through parameters
- You are implementing in-place algorithms that modify data structures directly
Language-specific best practices:
| Language | Default | Reference Syntax | Recommendation |
|---|---|---|---|
| C | Value | Pointers (*, &) | Use const pointers for read-only large objects |
| C++ | Value | & references | Use const & for read-only, & for modification |
| Java | Value (primitives), Sharing (objects) | N/A | Primitives are always value; use objects/arrays for modification |
| Python | Object reference (sharing) | N/A | Immutables behave like value; mutables behave like reference |
Algorithm
Decision checklist for choosing parameter passing strategy:
-
Does the function need to modify the caller's data?
- YES → Use pass by reference
- NO → Use pass by value
-
Is the data large (arrays, structures, objects)?
- YES → Use pass by reference (or
constreference) for efficiency - NO → Pass by value is fine
- YES → Use pass by reference (or
-
Do you need to return more than one value?
- YES → Use reference parameters for the extra return values
- NO → Use the normal return statement
-
Do you want the function to be pure (no side effects)?
- YES → Use pass by value
- NO → Pass by reference is acceptable
Code
#include <iostream>
#include <vector>
using namespace std;
// Pass by value — safe, no side effects
int square(int x) {
return x * x; // x is a copy, original is safe
}
// Pass by const reference — efficient, read-only access
int sumArray(const vector<int> &arr) {
int total = 0;
for (int val : arr) {
total += val;
}
return total; // No copy of the vector was made!
}
// Pass by reference — modification needed
void doubleEach(vector<int> &arr) {
for (int &val : arr) {
val *= 2; // Modifies original array elements
}
}
// Multiple return values via reference parameters
void getMinMax(const vector<int> &arr, int &minVal, int &maxVal) {
minVal = arr[0];
maxVal = arr[0];
for (int val : arr) {
if (val < minVal) minVal = val;
if (val > maxVal) maxVal = val;
}
}
int main() {
// Pass by value
int n = 5;
cout << "Square of " << n << " = " << square(n) << endl;
cout << "n is still " << n << endl; // n unchanged
// Pass by const reference (efficient read)
vector<int> nums = {3, 1, 4, 1, 5};
cout << "Sum: " << sumArray(nums) << endl;
// Pass by reference (modification)
doubleEach(nums);
cout << "Doubled: ";
for (int v : nums) cout << v << " "; // 6 2 8 2 10
cout << endl;
// Multiple returns via reference
int lo, hi;
getMinMax(nums, lo, hi);
cout << "Min: " << lo << ", Max: " << hi << endl;
return 0;
}from typing import List, Tuple
# Pass by value behavior (immutable types like int)
def square(x: int) -> int:
return x * x # x is a local name, original is safe
# Pass by reference behavior (mutable types like list)
def double_each(arr: List[int]) -> None:
"""Modifies the original list in place."""
for i in range(len(arr)):
arr[i] *= 2 # Mutates the original list!
# Multiple return values using tuple (Pythonic approach)
def get_min_max(arr: List[int]) -> Tuple[int, int]:
return min(arr), max(arr)
# Pass by value behavior
n = 5
print(f"Square of {n} = {square(n)}") # 25
print(f"n is still {n}") # 5 (unchanged)
# Pass by reference behavior (list is mutable)
nums = [3, 1, 4, 1, 5]
double_each(nums)
print(f"Doubled: {nums}") # [6, 2, 8, 2, 10] — original is modified!
# Multiple return values
lo, hi = get_min_max(nums)
print(f"Min: {lo}, Max: {hi}")import java.util.Arrays;
public class ParameterPassingSummary {
// Pass by value — safe, no side effects
static int square(int x) {
return x * x; // x is a copy
}
// Pass by sharing — array reference is copied, but array itself is shared
static int sumArray(int[] arr) {
int total = 0;
for (int val : arr) {
total += val;
}
return total; // Does not modify arr
}
// Modification through shared reference
static void doubleEach(int[] arr) {
for (int i = 0; i < arr.length; i++) {
arr[i] *= 2; // Modifies original array elements
}
}
// Multiple return values using array (Java workaround)
static int[] getMinMax(int[] arr) {
int min = arr[0], max = arr[0];
for (int val : arr) {
if (val < min) min = val;
if (val > max) max = val;
}
return new int[]{min, max};
}
public static void main(String[] args) {
// Pass by value
int n = 5;
System.out.println("Square of " + n + " = " + square(n));
System.out.println("n is still " + n); // unchanged
// Pass by sharing (array)
int[] nums = {3, 1, 4, 1, 5};
System.out.println("Sum: " + sumArray(nums));
// Modification through shared reference
doubleEach(nums);
System.out.println("Doubled: " + Arrays.toString(nums));
// [6, 2, 8, 2, 10]
// Multiple returns
int[] result = getMinMax(nums);
System.out.println("Min: " + result[0] + ", Max: " + result[1]);
}
}Complexity Analysis
Pass by Value:
- Time: O(size of data) to copy the argument. For primitives (int, char), this is O(1). For large arrays or objects, copying can be O(n).
- Space: O(size of data) for the copy. Each function call allocates space for the copy on the stack.
Pass by Reference:
- Time: O(1) to pass the address/reference, regardless of the data size. An address is typically 4 or 8 bytes.
- Space: O(1) for the reference itself. No copy of the data is made.
Practical impact: For large data structures (arrays with millions of elements, complex objects), pass by reference is dramatically more efficient. Passing a million-element array by value copies all million elements; passing it by reference copies just one pointer (8 bytes).