Pass by Value
In C, pass by value is the default mechanism for passing arguments to functions. When a variable is passed, the function receives a copy of that variable. Any modification inside the function affects only the copy—not the original value. Understanding this concept is essential for writing safe, predictable, and bug-free C programs.
Syntax:
return_type function_name(data_type parameter_name) {
// parameter_name is a COPY of the argument
// modifications here do NOT affect the original
}
// Calling the function
function_name(variable); // passes a copy of 'variable'
Basic Example
In the following example, the variable value is passed to the function
increment(). The function modifies its local copy, but the original variable
in main() remains unchanged.
#include <stdio.h>
void increment(int num) {
num = num + 1;
printf("Inside function: %d\n", num);
}
int main() {
int value = 5;
printf("Before function call: %d\n", value);
increment(value);
printf("After function call: %d\n", value);
return 0;
}
Output:
Before function call: 5
Inside function: 6
After function call: 5
value in main() is never modified because
increment() operates on a separate copy.
Why C Uses Pass by Value
Pass by value provides data safety. Since functions cannot accidentally modify the caller’s variables, programs become easier to understand and debug. This behavior is especially useful when working with critical values or constants.
- Prevents unintended side effects
- Makes function behavior predictable
- Easier debugging and testing
main() when passing them by value.
To update the original variable, you must use pointers (covered next).
How Pass by Value Works in Memory
Understanding what happens in memory during a function call helps solidify your understanding. When you call a function with pass by value, the following steps occur:
| Step | What Happens | Memory Location |
|---|---|---|
| 1. Function Call | New stack frame is created for the function | Stack grows downward |
| 2. Copy Arguments | Values are copied to function parameters | New memory in function's stack frame |
| 3. Execute Function | Function works with its local copies | Only function's stack frame affected |
| 4. Return | Stack frame is destroyed, copies are gone | Memory is reclaimed |
Define a Function That Tries to Modify
void modifyValue(int num) {
printf("Address of parameter num: %p\n", (void*)&num);
num = 999; // Modify the local copy
printf("Value of num after change: %d\n", num);
}
The function receives a copy of the original value in a new memory location.
When the function modifies num, it only changes this local copy.
The original variable in the calling function remains completely untouched.
This is the fundamental behavior of pass by value in C.
Call the Function and Observe
int main() {
int original = 42;
printf("Address of original: %p\n", (void*)&original);
modifyValue(original); // Pass the value
printf("Value of original after call: %d\n", original); // Still 42!
return 0;
}
Output:
Address of original: 0x7ffd5e8a4a5c
Address of parameter num: 0x7ffd5e8a4a3c // Different address!
Value of num after change: 999
Value of original after call: 42 // Unchanged!
num
lives at a different memory location than original. They are completely separate variables.
Multiple Parameters
Functions can accept multiple parameters, each passed by value independently. This is useful for operations that require several inputs.
Define Functions with Multiple Parameters:
double calculateArea(double length, double width) {
return length * width;
}
double calculatePerimeter(double length, double width) {
return 2 * (length + width);
}
Each parameter is a separate copy - none affect the original variables.
When you pass roomLength and roomWidth, the function
gets its own copies called length and width.
The original variables remain unchanged after the function call.
Use the Functions:
int main() {
double roomLength = 12.5, roomWidth = 8.3;
double area = calculateArea(roomLength, roomWidth); // 103.75
double perimeter = calculatePerimeter(roomLength, roomWidth); // 41.60
printf("Area: %.2f, Perimeter: %.2f\n", area, perimeter);
return 0;
}
Output:
Room dimensions: 12.5 x 8.3 meters
Area: 103.75 square meters
Perimeter: 41.60 meters
Returning Values: The Pass by Value Solution
If a function cannot modify its parameters (due to pass by value), how do we get results back? The answer is return values. The function computes a result and sends it back to the caller.
Define a Function That Returns a Value:
int addTen(int num) {
return num + 10; // Return the result
}
The function computes a new value and returns it - the original parameter is unchanged.
The return statement sends the computed result back to the caller.
This is the standard way to get results from functions when using pass by value.
The caller must capture this returned value to use it.
Capture the Returned Value:
int main() {
int value = 5;
value = addTen(value); // Capture the returned value
printf("New value: %d\n", value); // Output: 15
return 0;
}
Assign the function's return value to a variable to use the result.
Here we reassign value to hold the returned result.
Without capturing the return value, the computation would be lost.
This pattern is very common in C programming.
Pass by Value with Structures
Structures (structs) in C are also passed by value by default. This means the entire structure is copied, which can be inefficient for large structures.
Define a Structure:
struct Point {
int x;
int y;
};
Function Receives a COPY of the Structure:
void movePoint(struct Point p, int dx, int dy) {
p.x += dx;
p.y += dy; // Modifies the COPY, not original!
printf("Inside function: (%d, %d)\n", p.x, p.y);
}
The function receives a complete copy of the structure.
All fields (x and y) are copied to a new struct.
Any modifications inside the function only affect this copy.
The original structure in main() stays unchanged.
Test It:
int main() {
struct Point origin = {0, 0};
printf("Before: (%d, %d)\n", origin.x, origin.y); // (0, 0)
movePoint(origin, 5, 3);
printf("After: (%d, %d)\n", origin.x, origin.y); // Still (0, 0)!
return 0;
}
When to Use Pass by Value
| Scenario | Use Pass by Value? | Reason |
|---|---|---|
| Simple calculations | Yes | Safe and simple, return the result |
| Small data types (int, char, float) | Yes | Copying is fast and efficient |
| Read-only operations | Yes | No need to modify original |
| Large structures | No | Copying is expensive, use pointers |
| Need to modify original | No | Impossible with pass by value |
| Arrays | N/A | Arrays always decay to pointers |
Practice Questions: Pass by Value
Test your understanding of how pass by value works in C.
Given:
void change(int x) {
x = 10;
}
int main() {
int a = 5;
change(a);
printf("%d", a);
}
Task: What will be printed?
Show Solution
// Output: 5
// Explanation: 'a' is passed by value, so only a copy is modified.
Given:
void swap(int a, int b) {
int temp = a;
a = b;
b = temp;
}
int main() {
int x = 3, y = 7;
swap(x, y);
printf("%d %d", x, y);
}
Task: What will be printed and why?
Show Solution
// Output: 3 7
// Explanation: Only copies of x and y are swapped inside the function.
// The original variables remain unchanged.
Given:
struct Point { int x; int y; };
void movePoint(struct Point p, int dx, int dy) {
p.x += dx;
p.y += dy;
}
int main() {
struct Point origin = {0, 0};
movePoint(origin, 5, 3);
printf("(%d, %d)", origin.x, origin.y);
}
Task: Predict the output and explain why.
Show Solution
// Output: (0, 0)
// Explanation: Structures are also passed by value.
// The function modifies a copy, not the original structure.
Given:
void doubleValue(int x) {
x = x * 2;
}
int main() {
int num = 5;
doubleValue(num);
printf("%d", num); // Should print 10
}
Task: Fix this function so it correctly doubles the input.
Show Solution
// Solution 1: Return the result
int doubleValue(int x) {
return x * 2;
}
int main() {
int num = 5;
num = doubleValue(num); // Capture returned value
printf("%d", num); // Prints 10
}
// Solution 2: Use pointer (covered in next section)
void doubleValue(int *x) {
*x = *x * 2;
}
Pass by Reference
In C, you can simulate pass by reference using pointers. This allows a function to modify the original variable by passing its address. It's essential for tasks like swapping values or updating data inside a function.
*).
Syntax:
return_type function_name(data_type *pointer_name) {
*pointer_name = new_value; // modifies the ORIGINAL variable
}
// Calling the function
function_name(&variable); // passes the ADDRESS of 'variable'
Basic Pointer Parameter Example
#include <stdio.h>
void setZero(int *p) {
*p = 0; // Dereference and modify
}
int main() {
int x = 10;
printf("Before: %d\n", x);
setZero(&x); // Pass address of x
printf("After: %d\n", x);
return 0;
}
Output:
Before: 10
After: 0
*p lets you change the value at the address passed in, so x is updated in main.
The Classic Swap Problem
One of the most common interview questions is swapping two variables. This demonstrates why pass by reference is essential for certain operations.
void swapWrong(int a, int b) {
int temp = a;
a = b;
b = temp;
// Only swaps local copies!
}
int main() {
int x = 5, y = 10;
swapWrong(x, y);
printf("x=%d, y=%d\n", x, y);
// Output: x=5, y=10 (unchanged!)
return 0;
}
void swapCorrect(int *a, int *b) {
int temp = *a;
*a = *b;
*b = temp;
// Swaps actual values!
}
int main() {
int x = 5, y = 10;
swapCorrect(&x, &y);
printf("x=%d, y=%d\n", x, y);
// Output: x=10, y=5 (swapped!)
return 0;
}
Understanding the & and * Operators
Two operators are essential for pass by reference: the address-of operator (&)
and the dereference operator (*).
| Operator | Name | Purpose | Example |
|---|---|---|---|
& |
Address-of | Gets the memory address of a variable | &x returns address of x |
* |
Dereference | Gets/sets the value at an address | *ptr accesses value at ptr |
* |
Pointer declaration | Declares a pointer variable | int *ptr; |
Declare Variable and Pointer
int number = 42;
int *ptr = &number; // ptr stores the address of number
The & operator gets the memory address of number.
The pointer ptr stores this address as its value.
Now ptr "points to" the memory location where number lives.
Both variables are now connected through this address.
Access Values and Addresses
printf("Value of number: %d\n", number); // 42
printf("Address of number: %p\n", (void*)&number); // 0x7ffd...
printf("Value of ptr (address): %p\n", (void*)ptr);// Same address
printf("Value at ptr (*ptr): %d\n", *ptr); // 42
The *ptr dereferences the pointer to get the value at that address.
Dereferencing means "go to the address stored in the pointer and get the value there".
Since ptr points to number, *ptr gives us 42.
This is how we access data through pointers.
Modify Through Pointer
*ptr = 100; // Changes 'number' through the pointer
printf("number = %d\n", number); // Now 100!
Assigning to *ptr changes the original variable.
This works because ptr and number share the same memory.
When we write to *ptr, we're writing directly to that memory location.
This is the key to modifying variables through pointers.
Multiple Output Parameters
Functions can only return one value. But what if you need to return multiple values? Use pointer parameters as "output parameters" to store multiple results.
Define a Function with Multiple Outputs:
// Returns both quotient and remainder through pointers
void divide(int dividend, int divisor, int *quotient, int *remainder) {
*quotient = dividend / divisor;
*remainder = dividend % divisor;
}
Input parameters (dividend, divisor) are passed by value.
Output parameters (quotient, remainder) are pointers.
The function writes results directly to the memory locations provided.
This pattern allows returning multiple values from a single function.
Use the Function:
int main() {
int a = 17, b = 5;
int q, r; // Variables to receive the results
divide(a, b, &q, &r); // Pass addresses of q and r
printf("%d / %d = %d with remainder %d\n", a, b, q, r);
// Output: 17 / 5 = 3 with remainder 2
return 0;
}
The function stores results directly into q and r through their addresses.
We pass &q and &r to give the function access to these variables.
After the function returns, q contains 3 and r contains 2.
This is how C functions return multiple values.
Output:
17 / 5 = 3 with remainder 2
Success/Failure Pattern with Output Parameters
A common C pattern is to return a success/failure status while using an output parameter for the actual result. This allows for proper error handling.
Define a Safe Function with Error Handling:
// Returns true on success, false on error
// Result is stored in *result
bool safeDivide(int a, int b, int *result) {
if (b == 0) {
return false; // Division by zero!
}
*result = a / b;
return true;
}
The function returns a boolean for success/failure status. The actual result is stored through the pointer parameter. This pattern separates error handling from the computed result. Callers can check the return value before using the result.
Use the Function with Error Checking:
int main() {
int result;
// Successful division
if (safeDivide(10, 2, &result)) {
printf("10 / 2 = %d\n", result); // 10 / 2 = 5
} else {
printf("Error: Division by zero!\n");
}
// Failed division (divide by zero)
if (safeDivide(10, 0, &result)) {
printf("10 / 0 = %d\n", result);
} else {
printf("Error: Division by zero!\n"); // This prints
}
return 0;
}
Output:
10 / 2 = 5
Error: Division by zero!
Modifying Structures via Pointers
When working with structures, passing by pointer is essential for both efficiency (avoiding copies) and the ability to modify the original.
Define a Structure
struct Student {
char name[50];
int age;
float gpa;
};
A structure groups related data together into a single unit.
This Student struct holds name, age, and GPA fields.
Structures help organize complex data in a meaningful way.
They can be passed to functions just like simple variables.
Function to Modify Structure via Pointer
void updateGPA(struct Student *s, float newGPA) {
s->gpa = newGPA; // Arrow operator for pointer to struct
}
Use the arrow operator (->) to access members through a pointer.
The expression s->gpa is equivalent to (*s).gpa.
This modifies the actual structure in the caller's memory.
The arrow operator makes pointer-to-struct code much cleaner.
Function to Read Structure (const pointer)
void displayStudent(const struct Student *s) {
printf("Name: %s, Age: %d, GPA: %.2f\n", s->name, s->age, s->gpa);
}
Use const for read-only access - documents intent and prevents mistakes.
The compiler will catch any attempts to modify the structure.
This is a best practice for functions that only need to read data.
It makes your code safer and more self-documenting.
Using the Functions
int main() {
struct Student student = {"Alice Johnson", 20, 3.5};
displayStudent(&student); // Pass address for reading
updateGPA(&student, 3.8); // Pass address for modifying
displayStudent(&student); // Shows updated GPA
return 0;
}
Output:
Before update:
Name: Alice Johnson, Age: 20, GPA: 3.50
After update:
Name: Alice Johnson, Age: 20, GPA: 3.80
ptr->member
instead of (*ptr).member. Both are equivalent, but the arrow is cleaner.
NULL Pointer Safety
A major risk with pointer parameters is receiving a NULL pointer. Always validate pointers before dereferencing to prevent crashes.
❌ Unsafe: No NULL Check
void unsafeIncrement(int *ptr) {
*ptr += 1; // Crash if ptr is NULL!
}
Dereferencing NULL causes undefined behavior (usually a crash). NULL means the pointer doesn't point to valid memory. Attempting to read or write through NULL is a common bug. Always check for NULL before dereferencing pointers.
✓ Safe: Check for NULL
void safeIncrement(int *ptr) {
if (ptr != NULL) {
*ptr += 1;
}
}
Always validate pointers before using them. This check prevents the crash that would occur with NULL. If the pointer is NULL, the function simply does nothing. This is called defensive programming - handle bad inputs gracefully.
✓ Even Better: Return Status Code
int safeIncrementWithStatus(int *ptr) {
if (ptr == NULL) {
return -1; // Error code
}
*ptr += 1;
return 0; // Success
}
Returning a status lets the caller know if the operation succeeded. Here, -1 indicates an error and 0 indicates success. The caller can check the return value and handle errors appropriately. This pattern is common in C system programming and libraries.
Usage:
int main() {
int x = 5;
// Safe usage
safeIncrement(&x);
printf("x = %d\n", x); // x = 6
// This would crash with unsafeIncrement:
// unsafeIncrement(NULL);
// Safe version handles NULL gracefully
safeIncrement(NULL); // Does nothing, no crash
return 0;
}
Pass by Value vs Pass by Reference Comparison
| Aspect | Pass by Value | Pass by Reference (Pointer) |
|---|---|---|
| What is passed? | Copy of the value | Address (memory location) |
| Can modify original? | No | Yes |
| Memory usage | Creates a copy | Only pointer size (4-8 bytes) |
| Syntax | func(x) |
func(&x) |
| In function | param = value |
*param = value |
| Safety | Very safe | Risk of NULL dereference |
| Use case | Small data, read-only | Large data, need to modify |
Practice Questions: Pass by Reference
Test your understanding of reference passing in C functions.
Given:
#include <stdio.h>
void inc(int *n) {
*n = *n + 1;
}
int main() {
int a = 5;
inc(&a);
printf("%d\n", a);
return 0;
}
Task: What will be the output?
Show Solution
// Output: 6
// The function receives the address of 'a' and increments
// the value at that address, modifying the original variable.
Given:
void swap(int *a, int *b) {
int temp = *a;
*a = *b;
*b = temp;
}
int main() {
int x = 5, y = 10;
swap(&x, &y);
printf("x = %d, y = %d\n", x, y);
return 0;
}
Task: Predict the output and explain why this works.
Show Solution
// Output: x = 10, y = 5
// The function swaps values using pointers.
// It accesses and modifies the original variables through their addresses.
Given:
void set(int *p) {
*p = 5;
}
int main() {
int a = 10;
set(NULL);
printf("%d\n", a);
return 0;
}
Task: What happens when this code runs? How would you fix it?
Show Solution
// Result: Crash (segmentation fault)
// Dereferencing NULL causes undefined behavior.
// Fix: Add NULL check before dereferencing:
void set(int *p) {
if (p != NULL) {
*p = 5;
}
}
Given:
void safeSet(int *p) {
if (p != NULL) {
*p = 10;
}
}
int main() {
int a = 5;
safeSet(&a);
printf("%d\n", a);
return 0;
}
Task: What is the output and why is this approach better?
Show Solution
// Output: 10
// This is safer because we check for NULL before dereferencing.
// Always validate pointers to avoid crashes.
Passing Arrays
In C, arrays are always passed to functions as pointers to their first element. This means the function can modify the original array, but the array size information is not passed automatically. Understanding this is key to safe and effective array manipulation.
Syntax:
// Three equivalent ways to declare array parameters:
return_type function_name(data_type arr[], int size); // Most common
return_type function_name(data_type *arr, int size); // Pointer notation
return_type function_name(data_type arr[10], int size); // Size is ignored!
// Calling the function
function_name(array_name, array_size); // array decays to pointer
Basic Array Passing
When you pass an array to a function, C automatically converts it to a pointer to the first element. This process is called array decay.
#include <stdio.h>
void printArray(int arr[], int size) {
for (int i = 0; i < size; i++) {
printf("%d ", arr[i]);
}
printf("\n");
}
int main() {
int data[5] = {10, 20, 30, 40, 50};
printArray(data, 5); // Pass array and its size
return 0;
}
Output:
10 20 30 40 50
Why Size Must Be Passed Separately
A common beginner question: "Why can't the function figure out the array size?" The answer lies in how C handles arrays.
#include <stdio.h>
void demonstrateSizeProblem(int arr[]) {
// sizeof(arr) returns pointer size, NOT array size!
printf("Inside function: sizeof(arr) = %zu bytes\n", sizeof(arr));
printf("This is the size of a pointer, not the array!\n");
}
int main() {
int numbers[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
printf("In main: sizeof(numbers) = %zu bytes\n", sizeof(numbers));
printf("Array has %zu elements\n", sizeof(numbers) / sizeof(numbers[0]));
demonstrateSizeProblem(numbers);
return 0;
}
Output (on 64-bit system):
In main: sizeof(numbers) = 40 bytes
Array has 10 elements
Inside function: sizeof(arr) = 8 bytes
This is the size of a pointer, not the array!
sizeof(arr) inside a function gives you the
pointer size (typically 4 or 8 bytes), NOT the array size. This is a very common bug!
Modifying Arrays in Functions
Since arrays are passed as pointers, any modifications inside the function affect the original array. This is different from pass by value with simple variables.
Define Functions to Modify Arrays
// Double each element in the array
void doubleElements(int arr[], int size) {
for (int i = 0; i < size; i++) {
arr[i] *= 2; // Modifies original array!
}
}
// Set all elements to a specific value
void fillArray(int arr[], int size, int value) {
for (int i = 0; i < size; i++) {
arr[i] = value;
}
}
These functions modify the original array directly since arrays are passed as pointers. When you pass an array to a function, C passes a pointer to the first element. Any changes made inside the function affect the original array. This is different from simple variables which are copied.
Create a Helper Function for Printing
void printArray(int arr[], int size) {
printf("{ ");
for (int i = 0; i < size; i++) {
printf("%d ", arr[i]);
}
printf("}\n");
}
A reusable function to display array contents. This function iterates through the array and prints each element. Notice we must pass the size since arrays decay to pointers. The function cannot determine the array size on its own.
Test the Modifications
int main() {
int data[5] = {1, 2, 3, 4, 5};
printf("Original: ");
printArray(data, 5);
doubleElements(data, 5);
printf("After doubling: ");
printArray(data, 5);
fillArray(data, 5, 0);
printf("After fill with 0: ");
printArray(data, 5);
return 0;
}
Output:
Original: { 1 2 3 4 5 }
After doubling: { 2 4 6 8 10 }
After fill with 0: { 0 0 0 0 0 }
Common Array Operations
Here are some commonly needed array functions that demonstrate proper array passing patterns.
Function 1: Sum All Elements
int sumArray(const int arr[], int size) {
int sum = 0;
for (int i = 0; i < size; i++) {
sum += arr[i];
}
return sum;
}
Uses const since we only read the array, not modify it.
The const keyword prevents accidental modifications.
It also documents the function's intent to other programmers.
The compiler will catch any attempts to change the array.
Function 2: Find Maximum Element
int findMax(const int arr[], int size) {
if (size <= 0) return 0; // Handle empty array
int max = arr[0];
for (int i = 1; i < size; i++) {
if (arr[i] > max) {
max = arr[i];
}
}
return max;
}
Starts by assuming the first element is the maximum value. Then iterates through the remaining elements one by one. Each element is compared against the current maximum. If a larger value is found, it becomes the new maximum. This approach guarantees finding the true maximum in the array.
Function 3: Find Minimum Element
int findMin(const int arr[], int size) {
if (size <= 0) return 0;
int min = arr[0];
for (int i = 1; i < size; i++) {
if (arr[i] < min) {
min = arr[i];
}
}
return min;
}
Uses the same algorithm pattern as findMax function. Starts with the first element as the initial minimum. Compares each subsequent element against the current minimum. Updates the minimum when a smaller value is discovered. This mirror approach makes both functions easy to understand together.
Function 4: Calculate Average
double calculateAverage(const int arr[], int size) {
if (size <= 0) return 0.0;
return (double)sumArray(arr, size) / size;
}
Demonstrates code reuse by calling the existing sumArray() function. Divides the total sum by the array size to calculate the mean. Returns a double to preserve decimal precision in the result. Shows how functions can be composed to build more complex operations. This modular approach reduces code duplication and improves maintainability.
Using the Functions:
int main() {
int scores[6] = {85, 92, 78, 95, 88, 76};
int size = 6;
printf("Sum: %d\n", sumArray(scores, size)); // 514
printf("Max: %d\n", findMax(scores, size)); // 95
printf("Min: %d\n", findMin(scores, size)); // 76
printf("Average: %.2f\n", calculateAverage(scores, size)); // 85.67
return 0;
}
Output:
Scores: 85 92 78 95 88 76
Sum: 514
Max: 95
Min: 76
Average: 85.67
const for array parameters when the function
should not modify the array. This prevents accidental modifications and documents intent.
Passing 2D Arrays
Two-dimensional arrays require special syntax. You must specify the column size in the function parameter because the compiler needs it to calculate element positions.
Define Constants for Array Dimensions
#define ROWS 3
#define COLS 4
Preprocessor constants define array dimensions in one place. Changes to ROWS or COLS automatically update all usages. Makes the code self-documenting and easier to understand. Prevents magic numbers scattered throughout the codebase. This is a best practice for any fixed-size array dimensions.
Function with arr[][COLS] Syntax
// For 2D arrays, column size MUST be specified
void print2DArray(int arr[][COLS], int rows) {
for (int i = 0; i < rows; i++) {
for (int j = 0; j < COLS; j++) {
printf("%3d ", arr[i][j]);
}
printf("\n");
}
}
The column size must be specified because C stores 2D arrays in row-major order. The compiler needs COLS to calculate memory offsets for element access. Row size can be omitted because only column stride matters for addressing. This is why arr[][COLS] syntax requires the second dimension. The compiler computes element address as: base + (row * COLS + col) * sizeof(int).
Alternative Pointer Syntax
// Alternative: use pointer to array
void print2DArrayAlt(int (*arr)[COLS], int rows) {
for (int i = 0; i < rows; i++) {
for (int j = 0; j < COLS; j++) {
printf("%3d ", arr[i][j]);
}
printf("\n");
}
}
The notation int (*arr)[COLS] reads as "pointer to array of COLS integers".
Parentheses are crucial because int *arr[COLS] would mean array of pointers.
Both syntaxes produce identical machine code when compiled.
The pointer notation makes the type relationship more explicit.
Choose whichever syntax is clearer for your team and codebase.
Using the Functions
int main() {
int matrix[ROWS][COLS] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
print2DArray(matrix, ROWS);
print2DArrayAlt(matrix, ROWS); // Same output
return 0;
}
Output:
Using arr[][COLS] syntax:
1 2 3 4
5 6 7 8
9 10 11 12
Using (*arr)[COLS] syntax:
1 2 3 4
5 6 7 8
9 10 11 12
| Syntax | Meaning | Common Use |
|---|---|---|
int arr[] |
Pointer to int | 1D arrays |
int arr[][N] |
Pointer to array of N ints | 2D arrays with fixed columns |
int (*arr)[N] |
Same as above (explicit pointer) | 2D arrays, clearer syntax |
int **arr |
Pointer to pointer to int | Dynamically allocated 2D arrays |
Passing Strings (Character Arrays)
In C, strings are character arrays ending with a null terminator (\0).
They follow the same decay rules but have a special advantage: the null terminator
marks the end, so you don't always need to pass the size.
Function 1: Print String Info
// String functions don't need size - they look for '\0'
void printString(const char str[]) {
printf("String: %s\n", str);
printf("Length: %zu characters\n", strlen(str));
}
The strlen() function from string.h returns the string length. It counts characters by scanning until it finds the null terminator. The null character itself is not included in the returned count. This is why strings don't need explicit size parameters passed. Always ensure strings are properly null-terminated before using strlen().
Function 2: Count Vowels
int countVowels(const char str[]) {
int count = 0;
for (int i = 0; str[i] != '\0'; i++) {
char c = str[i];
if (c == 'a' || c == 'e' || c == 'i' || c == 'o' || c == 'u' ||
c == 'A' || c == 'E' || c == 'I' || c == 'O' || c == 'U') {
count++;
}
}
return count;
}
The loop continues as long as the current character is not null. The condition str[i] != '\0' automatically stops at string end. No size parameter is needed because strings are self-delimiting. This is a key advantage of null-terminated strings in C. Always use this pattern when iterating through strings character by character.
Function 3: Convert to Uppercase (Modifies Original)
void toUpperCase(char str[]) { // No const - we modify it!
for (int i = 0; str[i] != '\0'; i++) {
if (str[i] >= 'a' && str[i] <= 'z') {
str[i] = str[i] - 32; // ASCII conversion
}
}
}
This function modifies the original string in place. Without const, the function is allowed to change array contents. The absence of const signals to callers that data will be modified. This is an important documentation practice in C programming. Always omit const when your function intentionally modifies parameters.
Using the Functions:
int main() {
char message[] = "Hello, World!";
printString(message); // String: Hello, World!, Length: 13
printf("Vowels: %d\n", countVowels(message)); // 3
toUpperCase(message);
printf("After uppercase: %s\n", message); // HELLO, WORLD!
return 0;
}
Output:
String: Hello, World!
Length: 13 characters
Vowels: 3
After uppercase: HELLO, WORLD!
Array Parameter Best Practices
- Always pass array size as a parameter
- Use
constfor read-only arrays - Validate size before accessing elements
- Document expected array format
- Use meaningful parameter names
- Use
sizeof(arr)inside functions - Assume a specific array size
- Access elements without bounds checking
- Modify arrays without documenting it
- Forget the null terminator for strings
Practice Questions: Passing Arrays
Test your understanding of array passing in C functions.
Given:
#include <stdio.h>
void zero(int arr[], int size) {
for (int i = 0; i < size; i++) {
arr[i] = 0;
}
}
int main() {
int b[3] = {1, 2, 3};
zero(b, 3);
printf("b = {%d, %d, %d}\n", b[0], b[1], b[2]);
return 0;
}
Task: Predict the output.
Show Solution
// Output: b = {0, 0, 0}
// The function modifies the original array elements
// because arrays are passed by reference (as pointers).
Given:
int numbers[] = {10, 20, 30, 40, 50};
Task: Write a function that takes an array and its size, returns the sum of all elements.
Show Solution
int sumArray(int arr[], int size) {
int sum = 0;
for (int i = 0; i < size; i++) {
sum += arr[i];
}
return sum;
}
int main() {
int numbers[] = {10, 20, 30, 40, 50};
int result = sumArray(numbers, 5);
printf("Sum: %d\n", result); // Output: Sum: 150
return 0;
}
Given:
void printSize(int arr[]) {
printf("Size: %lu\n", sizeof(arr));
}
int main() {
int b[5] = {1, 2, 3, 4, 5};
printf("In main: %lu\n", sizeof(b));
printSize(b);
return 0;
}
Task: Explain why the sizes are different.
Show Solution
// Output on 64-bit system:
// In main: 20 (5 ints × 4 bytes = 20)
// Size: 8 (size of pointer, not array!)
// Explanation: When an array is passed to a function,
// it "decays" into a pointer. sizeof(arr) inside the
// function gives the pointer size, not the array size.
// This is why you must always pass the size separately!
Given:
void foo(int arr[], int n) { /* ... */ }
void bar(int *arr, int n) { /* ... */ }
Task: Are these declarations equivalent? Explain.
Show Solution
// Yes, they are EQUIVALENT!
// Both receive a pointer to the first element.
// int arr[] and int *arr are identical in function parameters.
// The [] syntax is just "syntactic sugar" - it makes the
// intent clearer that we expect an array, but the
// compiler treats them the same way.
Parameter Modifiers
C allows you to use modifiers like const and restrict with function parameters. These modifiers help you write safer and more efficient code by controlling how parameters are used inside functions.
const creates a "read-only" promise - the function cannot modify the data (like a "Do Not Edit" stamp on a document). restrict (added in C99) is a performance hint telling the compiler that this pointer is the only way to access that memory region, allowing better optimization. volatile tells the compiler the value might change unexpectedly (used with hardware registers). These modifiers make code safer and can improve performance.
Syntax:
// const - prevents modification (read-only)
void print_data(const int *ptr); // cannot modify *ptr
void print_array(const int arr[], int n); // cannot modify arr elements
// restrict - optimization hint (C99)
void copy(int *restrict dest, const int *restrict src, int n);
// Combining modifiers
void process(const int *restrict data, int size);
The const Keyword in Detail
The const keyword is one of the most important tools for writing safe C code.
It tells the compiler (and other programmers) that a value should not be modified.
Read-Only Function (with const):
// Function promises NOT to modify the array
void printArray(const int arr[], int size) {
for (int i = 0; i < size; i++) {
printf("%d ", arr[i]);
}
printf("\n");
// This would cause a compiler error:
// arr[0] = 999; // Error: assignment of read-only location
}
The const keyword tells the compiler this data should not change. Any attempt to modify through this pointer causes a compile error. This catches accidental modification bugs at compile time, not runtime. The const promise also helps other programmers understand your intent. It's a form of documentation that the compiler actually enforces.
Modifying Function (without const):
// Function WILL modify the array (no const)
void doubleArray(int arr[], int size) {
for (int i = 0; i < size; i++) {
arr[i] *= 2; // This is allowed
}
}
Without the const qualifier, the function can freely modify elements. This signals to callers that their data may be changed. The absence of const is intentional and meaningful documentation. Use this when your function's purpose includes modifying the array. Readers can immediately tell this function has side effects on the data.
Using Both Functions:
int main() {
int numbers[] = {1, 2, 3, 4, 5};
int size = 5;
printf("Original: ");
printArray(numbers, size);
doubleArray(numbers, size);
printf("Doubled: ");
printArray(numbers, size);
return 0;
}
Output:
Original: 1 2 3 4 5
Doubled: 2 4 6 8 10
const for parameters that should not be modified.
It documents intent, catches bugs at compile time, and enables certain optimizations.
Understanding const with Pointers
With pointers, const can apply to the data being pointed to,
the pointer itself, or both. The placement of const determines what is constant.
| Declaration | Pointer Changeable? | Data Changeable? | Description |
|---|---|---|---|
int *p |
Yes | Yes | Normal pointer |
const int *p |
Yes | No | Pointer to constant data |
int *const p |
No | Yes | Constant pointer to data |
const int *const p |
No | No | Constant pointer to constant data |
#include <stdio.h>
int main() {
int x = 10, y = 20;
// 1. Pointer to const: can't modify data through pointer
const int *ptr1 = &x;
// *ptr1 = 100; // Error: assignment of read-only location
ptr1 = &y; // OK: can change where it points
// 2. Const pointer: can't change where it points
int *const ptr2 = &x;
*ptr2 = 100; // OK: can modify data
// ptr2 = &y; // Error: assignment of read-only variable
// 3. Const pointer to const: can't do either
const int *const ptr3 = &x;
// *ptr3 = 100; // Error: assignment of read-only location
// ptr3 = &y; // Error: assignment of read-only variable
printf("x = %d\n", x);
return 0;
}
Output:
x = 100
const int *p = "p is a pointer to an int that is const" (can't modify data).
int *const p = "p is a const pointer to an int" (can't change pointer).
The restrict Keyword (C99)
The restrict keyword is a performance optimization hint for the compiler.
It promises that the pointer is the only way to access that memory,
allowing the compiler to generate more efficient code.
Without restrict (conservative compilation):
// Compiler must assume src and dest might overlap
void copyArraySlow(int *dest, const int *src, int n) {
for (int i = 0; i < n; i++) {
dest[i] = src[i];
}
}
Without restrict, the compiler must assume pointers might overlap. This forces conservative code generation with extra memory loads. The compiler can't cache values because another pointer might change them. Loop optimizations like vectorization may be disabled for safety. This is correct but potentially slower than optimized code.
With restrict (optimized compilation):
// Compiler knows they don't overlap, can optimize
void copyArrayFast(int *restrict dest, const int *restrict src, int n) {
for (int i = 0; i < n; i++) {
dest[i] = src[i];
}
}
The restrict keyword promises these pointers don't alias each other. The compiler can now assume no overlap between src and dest. This enables aggressive optimizations like SIMD vectorization. Loop iterations can be processed in parallel for better performance. Only use restrict when you can guarantee the pointers don't overlap.
Usage Example:
int main() {
int source[] = {1, 2, 3, 4, 5};
int destination[5];
copyArrayFast(destination, source, 5);
// Prints: Copied: 1 2 3 4 5
return 0;
}
restrict and the pointers actually DO overlap
(aliasing), you get undefined behavior. Only use it when you're certain!
Real-World Examples of Parameter Modifiers
The C standard library uses these modifiers extensively. Understanding them helps you read and use library functions correctly.
// From string.h - notice the modifier patterns
// strlen: only reads the string, uses const
size_t strlen(const char *s);
// strcpy: dest is modified, src is read-only, both use restrict
char *strcpy(char *restrict dest, const char *restrict src);
// memcpy: same pattern as strcpy
void *memcpy(void *restrict dest, const void *restrict src, size_t n);
// strcmp: compares two strings, neither modified
int strcmp(const char *s1, const char *s2);
// qsort: compare function uses const void pointers
void qsort(void *base, size_t nmemb, size_t size,
int (*compar)(const void *, const void *));
const parameters
are inputs (source), and non-const parameters are outputs (destination).
restrict indicates the pointers must not overlap.
Combining Multiple Modifiers
For maximum safety and performance, you can combine modifiers. This is common in high-performance code.
Example 1: Processing Arrays with restrict and const
// Read-only input, modifiable output, no overlap guaranteed
void processData(int *restrict output,
const int *restrict input,
int size,
int multiplier) {
for (int i = 0; i < size; i++) {
output[i] = input[i] * multiplier;
}
}
The const modifier ensures the input array remains unchanged. The restrict modifier promises no overlap between input and output. Together, they provide both safety guarantees and optimization hints. This pattern is common in high-performance data processing code. Standard library functions like memcpy use this exact combination.
Example 2: Maximum Protection with const pointer to const data
// Neither the data nor the pointer can be modified
void analyzeData(const int *const data, int size) {
int sum = 0;
for (int i = 0; i < size; i++) {
sum += data[i];
}
printf("Sum: %d, Average: %.2f\n", sum, (double)sum / size);
}
The first const (const int*) prevents modifying the pointed-to data. The second const (*const) prevents changing where the pointer points. This provides maximum protection for read-only analysis functions. Neither the array elements nor the pointer itself can be changed. Use this pattern when the function should be purely observational.
Using the Functions:
int main() {
int source[] = {1, 2, 3, 4, 5};
int result[5];
processData(result, source, 5, 3); // Multiply each by 3
analyzeData(result, 5); // Analyze the result
return 0;
}
// Output: Sum: 45, Average: 9.00
Output:
Result: 3 6 9 12 15
Sum: 45, Average: 9.00
When to Use Each Modifier
| Modifier | Use When | Example Use Case |
|---|---|---|
const |
Parameter should not be modified | Print functions, search functions |
restrict |
Pointers definitely don't overlap | Copy functions, transformation functions |
const + restrict |
Read-only input with no aliasing | Source parameter in copy operations |
volatile |
Value may change unexpectedly | Hardware registers, signal handlers |
- Use
constfor all read-only parameters (arrays, strings, structs) - Use
restrictin performance-critical code when pointers don't overlap - Document your assumptions when using
restrict - Look at standard library function signatures as examples
Practice Questions: Parameter Modifiers
Test your understanding of parameter modifiers in C functions.
Given:
void foo(const int x) {
x = 10; // What happens here?
}
Task: What error occurs and why?
Show Solution
// Compiler error: assignment of read-only variable 'x'
// The const keyword prevents modification at compile time.
// This provides safety by catching mistakes early.
Given:
void copy(int * restrict dest, const int * restrict src, int n) {
for (int i = 0; i < n; i++) {
dest[i] = src[i];
}
}
Task: Explain the purpose of restrict here.
Show Solution
// restrict tells the compiler that dest and src point to
// non-overlapping memory regions. This allows the compiler
// to perform aggressive optimizations (like vectorization).
// Without restrict, the compiler must assume dest and src
// might overlap, preventing many optimizations.
Given:
void analyze(const int * const p) {
// What can you do with p?
}
Task: Explain what each const means.
Show Solution
// const int * const p means:
// 1. First const: Cannot modify the VALUE pointed to (*p = 5 is error)
// 2. Second const: Cannot modify the POINTER itself (p = &other is error)
// You can only READ through the pointer, and cannot reassign it.
Given:
void printString(const char *str) {
printf("%s\n", str);
}
Task: Why is const important here?
Show Solution
// Benefits of using const:
// 1. Documents intent: The function won't modify the string
// 2. Safety: Compiler catches accidental modifications
// 3. Flexibility: Can accept both char[] and string literals
// 4. Optimization: Compiler can make better optimizations
Advanced Parameter Patterns
Beyond the basics, C offers powerful patterns for working with function parameters. These advanced techniques are essential for writing professional-grade code that is efficient, maintainable, and handles complex data structures.
5.1 Function Pointers as Parameters
A function pointer holds the address of a function. When passed as a parameter, it allows one function to call another function that's chosen at runtime. This is the foundation of callback mechanisms in C.
Define the Function Pointer Type
// Syntax: return_type (*pointer_name)(parameter_types)
// Function pointer type for functions taking int and returning int
typedef int (*MathOperation)(int, int);
The typedef creates an alias called MathOperation for function pointers. Any function matching this signature can be stored in this type. The signature specifies two int parameters and an int return value. This makes function pointer declarations much more readable. Without typedef, the syntax for function pointers is quite complex.
Create Functions That Match the Signature
int add(int a, int b) { return a + b; }
int subtract(int a, int b) { return a - b; }
int multiply(int a, int b) { return a * b; }
int divide(int a, int b) { return b != 0 ? a / b : 0; }
All four functions have identical signatures matching MathOperation. They each accept two int parameters named a and b. Each returns an int result from their respective operation. This uniformity allows them to be used interchangeably as callbacks. The compiler ensures type safety when assigning to MathOperation variables.
Create a Function That Accepts Function Pointer
int calculate(int x, int y, MathOperation op) {
return op(x, y); // Call the function through the pointer
}
The calculate function accepts a function pointer as its third parameter. It doesn't know which specific operation will be performed at compile time. The actual function is determined at runtime by the caller's choice. Using op(x, y) invokes whatever function was passed in. This is the strategy pattern in C - choosing algorithms at runtime.
Use the Function Pointer
int main() {
printf("10 + 5 = %d\n", calculate(10, 5, add)); // 15
printf("10 - 5 = %d\n", calculate(10, 5, subtract)); // 5
printf("10 * 5 = %d\n", calculate(10, 5, multiply)); // 50
printf("10 / 5 = %d\n", calculate(10, 5, divide)); // 2
return 0;
}
Each call to calculate() passes a different math function. The same calculate() interface handles addition, subtraction, and more. This demonstrates runtime polymorphism in C using function pointers. Adding new operations requires only defining new matching functions. The core calculate() logic never needs to change.
- Callbacks: Pass a function to be called when an event occurs
- Strategy Pattern: Choose different algorithms at runtime
- Generic Algorithms: Write functions that work with any comparison logic
- Event Handlers: Register functions to respond to user input
Real-World Example: Custom Sorting with qsort()
The standard library's qsort() function uses a function pointer to compare
elements, making it work with any data type:
Include Required Headers
#include <stdio.h>
#include <stdlib.h> // For qsort()
#include <string.h> // For strcmp()
The stdlib.h header provides the qsort() generic sorting function. The string.h header provides strcmp() for comparing strings. These are standard C library headers available on all platforms. Including them gives access to many useful utility functions. Always include the correct headers to avoid implicit declarations.
Write Comparison Functions
// Comparison function for integers (ascending)
int compareInts(const void *a, const void *b) {
return (*(int*)a - *(int*)b);
}
// Comparison function for integers (descending)
int compareIntsDesc(const void *a, const void *b) {
return (*(int*)b - *(int*)a);
}
// Comparison function for strings
int compareStrings(const void *a, const void *b) {
return strcmp(*(const char**)a, *(const char**)b);
}
Comparison functions receive const void* because qsort works with any type. You must cast these generic pointers to your actual data type. Return a negative value if the first argument is less than the second. Return a positive value if the first argument is greater than the second. Return zero if both arguments are equal in sort order.
Sort Integer Array
int numbers[] = {64, 34, 25, 12, 22, 11, 90};
int n = sizeof(numbers) / sizeof(numbers[0]);
qsort(numbers, n, sizeof(int), compareInts);
printf("Sorted ascending: ");
for (int i = 0; i < n; i++) {
printf("%d ", numbers[i]);
}
printf("\n"); // Output: 11 12 22 25 34 64 90
The qsort function requires four arguments to sort any array. First: pointer to the array base address. Second: number of elements in the array. Third: size of each element in bytes using sizeof. Fourth: pointer to a comparison function for ordering.
Sort String Array
const char *names[] = {"Charlie", "Alice", "Bob", "Diana"};
int nameCount = sizeof(names) / sizeof(names[0]);
qsort(names, nameCount, sizeof(char*), compareStrings);
printf("Sorted names: ");
for (int i = 0; i < nameCount; i++) {
printf("%s ", names[i]);
}
printf("\n"); // Output: Alice Bob Charlie Diana
For an array of strings, each element is a char* pointer. The element size is sizeof(char*), not the string length. We're sorting pointers to strings, not the strings themselves. The comparison function receives pointers to char* (i.e., char**). This double indirection requires casting to (const char**) first.
5.2 Variadic Functions (Variable Arguments)
Variadic functions can accept a variable number of arguments. You've
already used one - printf()! The <stdarg.h> header
provides macros to work with variable arguments.
Include Headers and Declare Function
#include <stdio.h>
#include <stdarg.h>
// Variadic function to sum any number of integers
int sum(int count, ...) {
The three dots (...) in the parameter list indicate variable arguments. At least one fixed parameter must come before the ellipsis. The count parameter tells the function how many extra arguments follow. This is necessary because C has no built-in way to count varargs. The function signature clearly shows it accepts a flexible number of values.
Initialize and Process Arguments
va_list args; // Declare variable argument list
va_start(args, count); // Initialize with last fixed parameter
int total = 0;
for (int i = 0; i < count; i++) {
total += va_arg(args, int); // Get next argument as int
}
va_end(args); // Clean up
return total;
}
The va_start macro initializes the va_list for argument retrieval. Pass the last fixed parameter name so it knows where varargs begin. The va_arg macro retrieves the next argument with the specified type. Always call va_end when finished to clean up internal state. These macros handle the low-level stack manipulation for you.
Another Example - Finding Maximum
int max(int count, ...) {
va_list args;
va_start(args, count);
int maximum = va_arg(args, int); // First value is initial max
for (int i = 1; i < count; i++) {
int value = va_arg(args, int);
if (value > maximum) {
maximum = value;
}
}
va_end(args);
return maximum;
}
This function demonstrates finding the maximum among variable arguments. The first value retrieved becomes the initial maximum for comparison. Each subsequent value is compared against the current maximum. If a larger value is found, it replaces the current maximum. The loop uses count to know exactly how many arguments to process.
Using Variadic Functions
int main() {
printf("Sum of 1,2,3 = %d\n", sum(3, 1, 2, 3)); // 6
printf("Sum of 1,2,3,4,5 = %d\n", sum(5, 1, 2, 3, 4, 5)); // 15
printf("Max of 5,2,8,1 = %d\n", max(4, 5, 2, 8, 1)); // 8
return 0;
}
The first argument is always the count of values that follow. Callers must correctly specify how many arguments they're passing. sum(3, 1, 2, 3) means "sum the next 3 values: 1, 2, and 3". Getting the count wrong leads to undefined behavior. This pattern is common but requires careful attention from callers.
| Macro | Purpose | Usage |
|---|---|---|
va_list |
Type for variable argument list | va_list args; |
va_start() |
Initialize the list | va_start(args, lastFixed); |
va_arg() |
Get next argument of specified type | int val = va_arg(args, int); |
va_end() |
Clean up the list | va_end(args); |
va_copy() |
Copy the list (C99+) | va_copy(dest, src); |
Building a Custom Printf
Function Declaration and Setup
#include <stdio.h>
#include <stdarg.h>
// Simplified printf supporting %d, %s, %c
void myPrintf(const char *format, ...) {
va_list args;
va_start(args, format);
The format string is the last fixed parameter before the varargs. We pass "format" to va_start because arguments follow after it. The format string will be parsed to find format specifiers. Each specifier indicates the type of the next argument to retrieve. This mimics how the standard printf() function works internally.
Process Format String Character by Character
while (*format != '\0') {
if (*format == '%') {
format++; // Move past '%'
switch (*format) {
case 'd':
printf("%d", va_arg(args, int));
break;
case 's':
printf("%s", va_arg(args, char*));
break;
case 'c':
printf("%c", va_arg(args, int)); // char promoted to int
break;
case '%':
putchar('%');
break;
}
} else {
putchar(*format);
}
format++;
}
va_end(args);
}
When the % character is found, the next character is the format specifier. Each specifier (d, s, c) tells us what type to retrieve with va_arg. %d means retrieve an int, %s means retrieve a char*, and so on. The switch statement handles each supported format specifier. The %% sequence outputs a literal percent sign without consuming an argument.
Using the Custom Printf
int main() {
myPrintf("Hello, %s! You are %d years old.\n", "Alice", 25);
myPrintf("Grade: %c, Score: %d%%\n", 'A', 95);
return 0;
}
Our myPrintf function works identically to standard printf for supported formats. The format string contains both literal text and format specifiers. Arguments after the format string match the specifiers in order. This demonstrates how variadic functions enable flexible APIs. The real printf supports many more format options and modifiers.
5.3 Opaque Pointers for Data Hiding
Opaque pointers hide implementation details from users of your API. The header file declares a pointer to an incomplete type, and only the implementation file knows the actual structure definition.
// Only declare the type, don't define it
typedef struct Stack Stack;
// Public API - users only work with Stack*
Stack* stack_create(int capacity);
void stack_destroy(Stack *s);
void stack_push(Stack *s, int value);
int stack_pop(Stack *s);
int stack_peek(const Stack *s);
int stack_isEmpty(const Stack *s);
int stack_size(const Stack *s);
#include "stack.h"
#include <stdlib.h>
// Private definition - only known here
struct Stack {
int *data;
int top;
int capacity;
};
Stack* stack_create(int capacity) {
Stack *s = malloc(sizeof(Stack));
s->data = malloc(capacity * sizeof(int));
s->top = -1;
s->capacity = capacity;
return s;
}
void stack_push(Stack *s, int value) {
if (s->top < s->capacity - 1) {
s->data[++s->top] = value;
}
}
int stack_pop(Stack *s) {
if (s->top >= 0) {
return s->data[s->top--];
}
return -1; // Error value
}
- Encapsulation: Users can't access internal data directly
- Binary Compatibility: Change struct without recompiling users
- Reduced Dependencies: Users don't need internal headers
- Easier Testing: Mock implementations are simpler
5.4 Compound Literals as Parameters (C99+)
Compound literals create unnamed objects that can be passed directly to functions without declaring separate variables first.
Define Structure and Functions
#include <stdio.h>
struct Point { int x; int y; };
void printPoint(struct Point p) {
printf("Point(%d, %d)\n", p.x, p.y);
}
void printArray(int arr[], int size) {
for (int i = 0; i < size; i++) {
printf("%d ", arr[i]);
}
printf("\n");
}
These are ordinary functions that accept struct or array parameters. printPoint takes a Point struct by value, copying the entire structure. printArray takes an array pointer and size for iteration. Both functions will be used to demonstrate compound literals. The function definitions remain unchanged when using compound literals.
Traditional vs Compound Literal
// Traditional way - requires separate variable
struct Point p1 = {10, 20};
printPoint(p1);
// Using compound literal - no variable needed!
printPoint((struct Point){30, 40});
Compound literals let you create and pass a struct in one expression. The syntax (struct Point){30, 40} creates an unnamed temporary Point. This object exists for the duration of the expression or block. No separate variable declaration is needed for single-use values. This makes function calls more concise and reduces code clutter.
Passing Arrays and Getting Pointers
// Pass array directly without declaring a variable
printArray((int[]){1, 2, 3, 4, 5}, 5);
// Pointer to compound literal (valid in same block)
struct Point *ptr = &(struct Point){50, 60};
printf("Via pointer: Point(%d, %d)\n", ptr->x, ptr->y);
You can take the address of a compound literal with the & operator. The resulting pointer is valid within the current block scope. Once the block ends, the compound literal ceases to exist. Using the pointer after the block causes undefined behavior. Compound literals for arrays work the same way with implicit size.
5.5 Designated Initializers in Parameters (C99+)
Combined with compound literals, designated initializers allow you to specify which struct members to initialize, leaving others as zero.
Define a Configuration Struct
#include <stdio.h>
struct Config {
int width;
int height;
int depth;
int color;
int alpha;
};
This struct has five fields representing configuration options. Some fields like depth, color, and alpha may have sensible defaults. Designated initializers let you set only the fields you care about. Unspecified fields are automatically initialized to zero. This pattern is excellent for configuration with many optional settings.
Create a Function That Accepts Config
void configure(struct Config cfg) {
printf("Config: %dx%dx%d, color=%d, alpha=%d\n",
cfg.width, cfg.height, cfg.depth, cfg.color, cfg.alpha);
}
The function receives the entire struct by value on the stack. It can access all fields regardless of how they were initialized. Fields set by the caller contain their specified values. Fields not specified by the caller contain zero by default. This separation allows flexible, readable function calls.
Use Designated Initializers
// Only specify the values you care about
configure((struct Config){.width = 800, .height = 600});
// Output: Config: 800x600x0, color=0, alpha=0
// More explicit configuration
configure((struct Config){
.width = 1920,
.height = 1080,
.color = 0xFF0000,
.alpha = 255
});
Use .fieldname = value to initialize specific fields. Unspecified fields are automatically set to zero.
{800, 600, 32, 0xFF, 255} means,
{.width=800, .height=600} is crystal clear!
5.6 Simulating Default Parameters in C
Unlike C++ or Python, C does not support default parameter values natively. However, there are several patterns to simulate this behavior, making your APIs more flexible and user-friendly.
Method 1: Wrapper Functions
Create multiple functions with different parameter counts, where simpler versions call the full version with default values.
Define the Full Function
// Full function with all parameters
void drawRectangle(int x, int y, int width, int height, int color, int filled) {
printf("Rectangle at (%d,%d), size %dx%d, color=%d, filled=%d\n",
x, y, width, height, color, filled);
}
This is the complete function with all parameters explicitly specified. Every aspect of the rectangle is configurable by the caller. This function will be called by the wrapper functions. It serves as the single source of truth for the implementation. All variants ultimately delegate to this full version.
Create Wrapper Functions with Defaults
// Wrapper with default color (black) and filled (false)
void drawRectangleSimple(int x, int y, int width, int height) {
drawRectangle(x, y, width, height, 0x000000, 0);
}
// Wrapper with default filled (false)
void drawRectangleColored(int x, int y, int width, int height, int color) {
drawRectangle(x, y, width, height, color, 0);
}
Each wrapper provides a simplified interface for common use cases. Default values are hardcoded in the wrapper function calls. Callers choose the wrapper that matches their needs. This pattern is used extensively in the C standard library. For example, malloc() is simpler than the full calloc() interface.
Using the Functions
int main() {
// Full control - specify everything
drawRectangle(0, 0, 100, 50, 0xFF0000, 1);
// Simple version - uses default color and filled
drawRectangleSimple(10, 10, 80, 40);
// Colored version - uses default filled
drawRectangleColored(20, 20, 60, 30, 0x00FF00);
return 0;
}
Method 2: Macro-Based Defaults
Use preprocessor macros to provide default values when arguments are omitted. This approach uses variadic macros and token counting.
Define the Core Function
// The actual implementation
void greet_impl(const char *name, const char *greeting, int times) {
for (int i = 0; i < times; i++) {
printf("%s, %s!\n", greeting, name);
}
}
The implementation function has a suffix like _impl to distinguish it. All parameters are required for this underlying function. The macro will provide defaults and call this function. This separation keeps the logic clean and testable. Users interact with the macro, not this function directly.
Create Macros for Default Arguments
// Macro overloading based on argument count
#define greet1(name) greet_impl(name, "Hello", 1)
#define greet2(name, greeting) greet_impl(name, greeting, 1)
#define greet3(name, greeting, times) greet_impl(name, greeting, times)
// Argument counting macro
#define GET_MACRO(_1, _2, _3, NAME, ...) NAME
#define greet(...) GET_MACRO(__VA_ARGS__, greet3, greet2, greet1)(__VA_ARGS__)
Multiple macros handle different numbers of arguments. GET_MACRO selects the right version based on argument count. The final greet() macro dispatches to the correct version. This technique is powerful but can be complex to debug. Compiler errors may be less clear when macros are involved.
Using the Macro
int main() {
greet("World"); // Hello, World! (1 time)
greet("Alice", "Good morning"); // Good morning, Alice! (1 time)
greet("Bob", "Hi", 3); // Hi, Bob! (3 times)
return 0;
}
Method 3: Struct with Defaults (Recommended)
Use a configuration struct with a default initializer. This is the cleanest and most maintainable approach for complex functions.
Define the Options Struct
// Options struct with all configurable parameters
typedef struct {
int width;
int height;
int color;
int borderWidth;
int filled;
float opacity;
} WindowOptions;
Group all optional parameters into a single struct. Each field represents one configurable aspect. This scales well when you have many optional parameters. Adding new options doesn't change existing call sites. The struct serves as self-documenting configuration.
Create a Default Options Function or Macro
// Function that returns default options
WindowOptions defaultWindowOptions(void) {
return (WindowOptions){
.width = 800,
.height = 600,
.color = 0xFFFFFF,
.borderWidth = 1,
.filled = 1,
.opacity = 1.0f
};
}
// Or use a macro for compile-time initialization
#define DEFAULT_WINDOW_OPTIONS (WindowOptions){ \
.width = 800, .height = 600, \
.color = 0xFFFFFF, .borderWidth = 1, \
.filled = 1, .opacity = 1.0f \
}
The defaults function returns a fully initialized struct. Callers can modify only the fields they care about. The macro version works at compile time for static initialization. Both approaches ensure all fields have sensible values. This pattern is common in graphics and system libraries.
The Function That Uses Options
void createWindow(const char *title, WindowOptions opts) {
printf("Creating window '%s'\n", title);
printf(" Size: %dx%d\n", opts.width, opts.height);
printf(" Color: 0x%06X, Border: %d\n", opts.color, opts.borderWidth);
printf(" Filled: %s, Opacity: %.1f\n",
opts.filled ? "yes" : "no", opts.opacity);
}
The function accepts the title and an options struct. It doesn't need to know which fields were customized. All fields are guaranteed to have valid values. This makes the function implementation straightforward. The options struct can be reused across multiple calls.
Using with Custom Values
int main() {
// Use all defaults
createWindow("Default Window", defaultWindowOptions());
// Customize some options
WindowOptions opts = defaultWindowOptions();
opts.width = 1920;
opts.height = 1080;
opts.opacity = 0.9f;
createWindow("Custom Window", opts);
// One-liner with designated initializers (C99+)
createWindow("Quick Window", (WindowOptions){
.width = 640, .height = 480,
.color = 0x000000, .borderWidth = 2,
.filled = 0, .opacity = 0.8f
});
return 0;
}
Method 4: Sentinel Values
Use special values (like -1 or NULL) to indicate "use default". The function checks for these sentinel values and substitutes defaults.
Function with Sentinel Detection
void printMessage(const char *msg, int times, const char *prefix) {
// Use defaults for sentinel values
if (times <= 0) times = 1; // Default: 1 time
if (prefix == NULL) prefix = "INFO"; // Default: "INFO"
for (int i = 0; i < times; i++) {
printf("[%s] %s\n", prefix, msg);
}
}
The function checks each parameter for sentinel values. Negative or zero times triggers the default of 1. NULL prefix triggers the default "INFO" string. This approach works well for simple functions. Be careful that sentinel values don't conflict with valid inputs.
Using Sentinel Values
int main() {
printMessage("Hello", 3, "DEBUG"); // [DEBUG] Hello (3 times)
printMessage("Warning!", 0, "WARN"); // [WARN] Warning! (1 time, default)
printMessage("Test", 2, NULL); // [INFO] Test (2 times, default prefix)
printMessage("Default", 0, NULL); // [INFO] Default (all defaults)
return 0;
}
| Method | Pros | Cons | Best For |
|---|---|---|---|
| Wrapper Functions | Simple, type-safe, clear intent | Many function names, code duplication | 2-3 common parameter combinations |
| Macro Overloading | Single name, flexible | Complex, hard to debug, cryptic errors | Library APIs, advanced users |
| Struct with Defaults | Scalable, self-documenting, maintainable | More verbose for simple cases | Many optional parameters |
| Sentinel Values | Simple, backward compatible | Sentinel may conflict with valid values | Legacy code, simple functions |
Practice Questions: Advanced Patterns
Test your understanding of advanced parameter patterns.
Given:
int square(int x) { return x * x; }
int cube(int x) { return x * x * x; }
int apply(int (*func)(int), int value) {
return func(value);
}
int main() {
printf("%d\n", apply(square, 5));
printf("%d\n", apply(cube, 3));
}
Task: Predict the output.
Show Solution
// Output:
// 25 (square(5) = 5 * 5 = 25)
// 27 (cube(3) = 3 * 3 * 3 = 27)
Given:
double average(int count, ...) {
// Your code here
}
Task: Complete this function to return the average of all arguments.
Show Solution
double average(int count, ...) {
va_list args;
va_start(args, count);
double sum = 0;
for (int i = 0; i < count; i++) {
sum += va_arg(args, int);
}
va_end(args);
return sum / count;
}
// Usage:
// average(4, 10, 20, 30, 40) returns 25.0
Given:
struct Point p1 = {1, 2}; // Line A
struct Point *p2 = &p1; // Line B
printPoint((struct Point){3, 4}); // Line C
struct Point p3; // Line D
p3.x = 5; p3.y = 6; // Line E
Task: Which line uses a compound literal?
Show Solution
// Line C uses a compound literal.
// (struct Point){3, 4} creates an unnamed struct Point
// object that's passed directly to the function.
Given:
double arr[] = {3.14, 1.41, 2.71, 1.73};
Task: Write a comparison function to sort doubles in descending order.
Show Solution
int compareDoubles(const void *a, const void *b) {
double da = *(const double*)a;
double db = *(const double*)b;
// For descending order, return negative when a > b
if (da > db) return -1;
if (da < db) return 1;
return 0;
}
// Usage:
double arr[] = {3.14, 1.41, 2.71, 1.73};
qsort(arr, 4, sizeof(double), compareDoubles);
// Result: {3.14, 2.71, 1.73, 1.41}
Common Mistakes and Best Practices
Even experienced programmers make mistakes with function parameters. This section covers the most common pitfalls and provides guidelines for writing clean, safe, and maintainable code.
6.1 Common Mistakes
❌ Wrong:
void increment(int *p) {
p = p + 1; // Changes local copy of pointer!
}
int main() {
int x = 5;
increment(&x);
printf("%d\n", x); // Still 5!
}
✓ Correct:
void increment(int *p) {
*p = *p + 1; // Dereference to modify value
}
int main() {
int x = 5;
increment(&x);
printf("%d\n", x); // Now 6!
}
❌ Wrong:
int* createNumber() {
int num = 42;
return # // DANGER! num doesn't exist
// after function returns
}
int main() {
int *p = createNumber();
printf("%d\n", *p); // Undefined behavior!
}
✓ Correct:
int* createNumber() {
int *num = malloc(sizeof(int));
*num = 42;
return num; // Heap memory persists
}
int main() {
int *p = createNumber();
printf("%d\n", *p); // 42
free(p); // Don't forget to free!
}
❌ Wrong:
void printSize(int arr[]) {
// arr is actually a pointer here!
int size = sizeof(arr) / sizeof(arr[0]);
printf("Size: %d\n", size); // Wrong!
}
int main() {
int nums[10] = {0};
printSize(nums); // Prints 1 or 2, not 10!
}
✓ Correct:
void printSize(int arr[], int size) {
printf("Size: %d\n", size);
}
int main() {
int nums[10] = {0};
int size = sizeof(nums) / sizeof(nums[0]);
printSize(nums, size); // Prints 10
}
❌ Wrong:
void processData(int *data, int size) {
for (int i = 0; i < size; i++) {
data[i] *= 2; // Crash if data is NULL!
}
}
int main() {
processData(NULL, 5); // CRASH!
}
✓ Correct:
void processData(int *data, int size) {
if (data == NULL || size <= 0) {
return; // Defensive programming
}
for (int i = 0; i < size; i++) {
data[i] *= 2;
}
}
int main() {
processData(NULL, 5); // Safe, does nothing
}
❌ Wrong:
void uppercase(const char *str) {
for (int i = 0; str[i]; i++) {
str[i] = toupper(str[i]); // Error!
}
}
✓ Correct:
void uppercase(char *str) { // Remove const
for (int i = 0; str[i]; i++) {
str[i] = toupper(str[i]); // OK
}
}
// Or create a copy:
char* uppercaseCopy(const char *str) {
char *result = strdup(str);
// ... modify result
return result;
}
6.2 Best Practices Summary
- Use
constfor read-only parameters - Always pass array size as a separate parameter
- Check pointers for
NULLbefore dereferencing - Use meaningful parameter names
- Document what each parameter does
- Prefer passing small structs by value
- Use pointers for large structs (>16 bytes)
- Initialize output parameters to safe defaults
- Return status codes for error handling
- Use typedef for complex function pointer types
- Return pointers to local variables
- Use
sizeof()on array parameters - Modify
constparameters (cast away const) - Use uninitialized pointer parameters
- Ignore compiler warnings about parameters
- Pass too many parameters (max 4-5)
- Mix input and output parameters randomly
- Forget to
free()dynamically allocated return values - Use global variables when parameters work
- Rely on parameter order instead of names
6.3 Parameter Count Guidelines
| Parameters | Recommendation | Action |
|---|---|---|
| 0-3 | Ideal | Keep as is |
| 4-5 | Acceptable | Consider grouping related params |
| 6+ | Too many | Refactor using structs or split function |
Refactoring Too Many Parameters
Before (Too Many Parameters):
void createWindow(
int x, int y,
int width, int height,
const char *title,
int borderStyle,
int bgColor,
int fgColor,
int flags
) {
// Complex implementation
}
After (Using Struct):
struct WindowConfig {
int x, y;
int width, height;
const char *title;
int borderStyle;
int bgColor, fgColor;
int flags;
};
void createWindow(struct WindowConfig cfg) {
// Same implementation, cleaner API
}
// Usage with designated initializers:
createWindow((struct WindowConfig){
.width = 800, .height = 600,
.title = "My App"
});
Practice Questions: Best Practices
Test your understanding of parameter best practices.
Given:
char* getName() {
char name[50];
scanf("%s", name);
return name;
}
Task: Identify the bug and explain how to fix it.
Show Solution
// Bug: Returning pointer to local array `name`.
// The array is destroyed when the function returns,
// leading to undefined behavior.
// Fix 1: Use malloc
char* getName() {
char *name = malloc(50);
scanf("%s", name);
return name;
}
// Fix 2: Pass buffer as parameter
void getName(char *buffer, int size) {
fgets(buffer, size, stdin);
}
Given:
void process(const int *arr, int size) {
for (int i = 0; i < size; i++) {
arr[i] = arr[i] * 2;
}
}
Task: Explain the compilation error and how to fix it.
Show Solution
// Error: Cannot modify `arr[i]` because `arr`
// points to const int. The `const` keyword means "read-only".
// Fix: Remove const if you need to modify
void process(int *arr, int size) {
for (int i = 0; i < size; i++) {
arr[i] = arr[i] * 2; // OK now
}
}
Given:
void mystery(int *p) {
int x = 100;
p = &x;
*p = 200;
}
int main() {
int num = 50;
mystery(&num);
printf("%d\n", num);
}
Task: Predict the output and explain why.
Show Solution
// Output: 50
// Explanation: The function receives a copy of the pointer.
// When `p = &x` executes, only the local copy changes
// to point to `x`. The original `num` is never modified.
Given:
void sendEmail(char *to, char *from, char *subject,
char *body, int priority, int encrypt,
int html, int attachCount, char **attachments);
Task: Refactor this function to use a better parameter design.
Show Solution
// Better: Use a configuration struct
struct EmailConfig {
const char *to;
const char *from;
const char *subject;
const char *body;
int priority;
int encrypt; // Could use bool
int html; // Could use bool
int attachCount;
const char **attachments;
};
void sendEmail(struct EmailConfig config);
// Usage:
sendEmail((struct EmailConfig){
.to = "user@example.com",
.from = "me@example.com",
.subject = "Hello",
.body = "Hi there!",
.priority = 1
// Other fields default to 0/NULL
});
Master Reference Table
This comprehensive reference table summarizes all parameter passing methods in C. Use this as a quick lookup when deciding which approach to use in your programs.
| Method | Syntax | Inside Function | Original Modified? | Use Case |
|---|---|---|---|---|
| Pass by Value | void f(int x) |
x = value |
No | Small data, read-only access |
| Pass by Pointer | void f(int *x) |
*x = value |
Yes | Modify caller's variable |
| Pass Array | void f(int arr[], int n) |
arr[i] = value |
Yes | Process collections |
| Pass 2D Array | void f(int arr[][N], int rows) |
arr[i][j] = value |
Yes | Matrices, tables |
| Pass String | void f(char *str) |
str[i] = 'c' |
Yes | Text processing |
| Pass Struct (value) | void f(struct S s) |
s.field = value |
No | Small structs (<16 bytes) |
| Pass Struct (pointer) | void f(struct S *s) |
s->field = value |
Yes | Large structs, modifications |
| Pass const Pointer | void f(const int *x) |
// read only |
No (protected) | Read large data efficiently |
| Function Pointer | void f(int (*op)(int)) |
result = op(x) |
N/A | Callbacks, strategies |
| Variadic | void f(int n, ...) |
va_arg(args, int) |
Varies | Variable argument count |
Quick Decision Guide
- YES → Use pointers (
int *x) - NO → Use value (
int x) or const pointer
- YES → Use pointer (avoids expensive copy)
- NO → Either approach is fine
- YES → Always pass pointer + size
- Arrays automatically decay to pointers
- YES → Use
constmodifier - Compiler will catch accidental modifications
Real-World Code Patterns
Pattern 1: Output Parameter
// Function returns status, modifies result via pointer
int divide(int a, int b, int *result) {
if (b == 0) {
return -1; // Error: division by zero
}
*result = a / b;
return 0; // Success
}
// Usage:
int quotient;
if (divide(10, 2, "ient) == 0) {
printf("Result: %d\n", quotient); // Result: 5
} else {
printf("Error!\n");
}
Pattern 2: In-Place Array Modification
// Modify array elements in place
void doubleAll(int arr[], int size) {
for (int i = 0; i < size; i++) {
arr[i] *= 2;
}
}
// Usage:
int numbers[] = {1, 2, 3, 4, 5};
doubleAll(numbers, 5);
// numbers is now {2, 4, 6, 8, 10}
Pattern 3: Swap Values
// Classic swap using pointers
void swap(int *a, int *b) {
int temp = *a;
*a = *b;
*b = temp;
}
// Generic swap for any type (advanced)
void genericSwap(void *a, void *b, size_t size) {
char temp[size]; // VLA (C99)
memcpy(temp, a, size);
memcpy(a, b, size);
memcpy(b, temp, size);
}
Pattern 4: Returning Multiple Values
// Find min and max in array
void findMinMax(const int arr[], int size, int *min, int *max) {
if (size <= 0) return;
*min = *max = arr[0];
for (int i = 1; i < size; i++) {
if (arr[i] < *min) *min = arr[i];
if (arr[i] > *max) *max = arr[i];
}
}
// Usage:
int data[] = {5, 2, 8, 1, 9, 3};
int minVal, maxVal;
findMinMax(data, 6, &minVal, &maxVal);
printf("Min: %d, Max: %d\n", minVal, maxVal); // Min: 1, Max: 9
Pattern 5: Callback for Custom Behavior
// Apply any operation to array elements
typedef int (*Operation)(int);
void applyToAll(int arr[], int size, Operation op) {
for (int i = 0; i < size; i++) {
arr[i] = op(arr[i]);
}
}
// Callback functions
int square(int x) { return x * x; }
int negate(int x) { return -x; }
int increment(int x) { return x + 1; }
// Usage:
int nums[] = {1, 2, 3, 4, 5};
applyToAll(nums, 5, square); // {1, 4, 9, 16, 25}
applyToAll(nums, 5, negate); // {-1, -4, -9, -16, -25}
applyToAll(nums, 5, increment); // {0, -3, -8, -15, -24}
Interactive Demos
Explore these interactive demonstrations to visualize how parameter passing works in C. Adjust values and see the results in real-time to solidify your understanding.
Pass by Value vs Reference Simulator
Enter a value and see how it behaves when passed by value (copy) versus by reference (pointer).
void addValue(int num) {
num = num + 10; // Modifies COPY
}
int main() {
int x = 42;
addValue(x);
// x is still 42
}
void addRef(int *ptr) {
*ptr = *ptr + 10; // Modifies ORIGINAL
}
int main() {
int x = 42;
addRef(&x);
// x is now 52
}
Memory Address Visualizer
See how variables and pointers are stored in memory. Click on memory cells to understand the relationship.
Stack Memory Layout
Pointer Operations
int x = 42;
Variable x stores value 42 at address 0x1000
int *ptr = &x;
Pointer ptr stores address 0x1000
*ptr = 100;
Dereference: Go to address 0x1000 and change value
Key Concepts:
&x- Address-of operator (get address)*ptr- Dereference operator (get value at address)- Pointers are variables that store memory addresses
- Changing
*ptrchanges the value at that address
Key Takeaways
Pass by Value
By default, C passes function arguments by value, meaning the function receives a copy and cannot modify the original variable.
Using Pointers
Passing pointers allows functions to modify variables in the caller by accessing their memory addresses.
Arrays as Parameters
When an array is passed to a function, it decays into a pointer to its first element - array size must be passed separately.
const Safety
Using const in function parameters prevents accidental modification and improves code safety and readability.
restrict Optimization
The restrict keyword (C99) allows the compiler to optimize pointer-based code when aliasing is guaranteed not to occur.
Pointer Risks
Improper pointer usage can lead to crashes - always validate pointers before dereferencing them.
Knowledge Check
Quick Quiz
Test your understanding of Function Parameters in C