Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 15

Pointers: Pass-by-Value vs. Pass-by-Reference

Recommended resources

Hello! Welcome back to our module on Pointers and Functions.

In the previous lesson, we established a powerful practical skill: passing a pointer to a function to modify a variable in the caller's scope. We saw this is essential for tasks like swapping values or efficiently updating large data structures without making expensive copies.

Today, we're going to build the precise theoretical framework around that technique. The learning outcome for this lesson is to explain pass-by-value vs. pass-by-reference semantics using pointers. We will clarify the terminology and, most importantly, understand exactly what C does under the hood. This level of precision is vital in technical interviews and for understanding the subtle differences between languages like C and C++, which is directly relevant to your background.

Pass-by-Value: The Default C Behavior

As we saw last time, C's default mechanism for passing arguments to functions is pass-by-value. This means the function receives a private copy of each argument's value. Any modification to the parameter inside the function affects only this local copy; the original variable in the calling scope remains untouched.

A classic swap function is the perfect way to see this in action. The function receives copies of the integer values, swaps them locally, and those changes are lost forever when the function returns.

To refresh this concept and visualize why it happens, let's review the memory mechanics. The following video explains the role of the stack and stack frames in isolating a function's local variables from its caller.

Pointers as function arguments - call by reference

This video from mycodeschool, 'Pointers as function arguments - call by reference', provides an excellent visual demonstration of the pass-by-value problem and the underlying memory model.

Please watch from 00:41 to 02:58 to see the problem illustrated. Then, watch the detailed explanation of stack frames from 04:21 to 11:12. This will be a good review of concepts you're familiar with but with a helpful animation.

As the video illustrates, when main calls a function like increment (or swap), a new stack frame is created. The arguments from main are copied into the parameters of the new function's stack frame. The function operates exclusively on these copies. When the function finishes, its stack frame is destroyed, taking the modified copies with it.

C Program Memory Layout
A typical memory layout for a C program. In the pass-by-value scenario, the local variables for both the calling and called functions reside in separate stack frames within the Stack segment.

Simulating Pass-by-Reference with Pointers

To overcome this limitation, we use pointers. By passing the memory address of a variable, the function gains the ability to reach outside its own stack frame and modify the original data. This is how we make our swap function work. This technique is often loosely called pass-by-reference in the context of C.

The following video walks through implementing a correct swap function using this method.

Pass by Value vs. Pass by Reference

This video, 'Pass by Value vs. Pass by Reference' from theteachr, clearly demonstrates the 'before' (pass-by-value) and 'after' (pass-by-pointer) versions of a swap function.

Watch from 01:06 to 05:46. Pay close attention to how the function signature is changed to accept pointers and how the * (dereference) and & (address-of) operators are used.

The key steps are:

  1. Caller: Passes the addresses of the variables (&a, &b).
  2. Function Definition: Declares its parameters as pointers (int *px, int *py).
  3. Function Body: Dereferences the pointers (*px, *py) to access and modify the values at the original addresses.

This achieves the effect of pass-by-reference. But is it true pass-by-reference? The answer is a crucial point of technical accuracy.


The Decisive Distinction: C is Always Pass-by-Value

Here is the most important concept for this lesson: C does not have pass-by-reference. C is always and exclusively a pass-by-value language.

When we pass a pointer to a function, we are not creating a reference or an alias to the original variable. Instead, we are passing the value of the pointer (i.e., the memory address it holds) by value.

The function receives a copy of the memory address. It can use this copied address to find and modify the original data. However, the pointer parameter inside the function is itself a distinct local variable.

The following article explains this distinction perfectly. Pay special attention to the comment section, which offers a crystal-clear technical clarification.

Passing by value, passing by reference in C

The article 'Passing by value, passing by reference in C' and its insightful comments clarify the correct terminology and mechanics.

Read the main article to see the failing and working swap examples. Then, carefully read the first comment (by 'Thomas') which provides the technically precise explanation that C only has pass-by-value, even with pointers. This is the key takeaway.

This distinction is not just academic. It has practical consequences. Since the function has a copy of the pointer, it cannot change the original pointer in the caller.

Consider this code:

void try_to_null_pointer(int* p_local) {
    // p_local is a local variable in this function's stack frame.
    // It contains a copy of the address passed by the caller.
    
    // This modifies the data AT the original address. This works.
    *p_local = 99; 

    // This only modifies the local pointer copy. 
    // It does NOT affect the original pointer in main().
    p_local = NULL; 
}

int main(void) {
    int val = 10;
    int* p_main = &val;

    printf("Before call: p_main points to address %p\n", (void*)p_main);
    
    try_to_null_pointer(p_main);

    // p_main will NOT be NULL here. It still points to val.
    printf("After call: p_main points to address %p\n", (void*)p_main); 
    
    // The value of val, however, will be 99.
    printf("Value of val is now %d\n", val);
    
    return 0;
}

This is why, as we'll see in a later module, if you want a function to modify a caller's pointer (e.g., to reallocate memory for it), you must pass a pointer to that pointer (int**).

Test your understanding!

A colleague shows you this code and is confused. They expect the allocate_memory function to make data_ptr in main point to a new block of memory, but the program crashes with a segmentation fault when main tries to use data_ptr.

#include <stdio.h>
#include <stdlib.h>

void allocate_memory(int* ptr) {
    ptr = (int*)malloc(10 * sizeof(int));
    // The programmer intends for this to modify the pointer in main.
}

int main(void) {
    int* data_ptr = NULL;
    allocate_memory(data_ptr);
    
    // This line will cause a crash.
    data_ptr[0] = 123; 
    printf("Value set to %d\n", data_ptr[0]);
    
    free(data_ptr);
    return 0;
}

Based on the distinction between true pass-by-reference and C's pass-by-value mechanism, explain in one or two sentences why the data_ptr in main remains NULL.

Show answer

The allocate_memory function receives a copy of data_ptr's value, which is NULL. It then assigns the address from malloc to its local copy (ptr), not to the original data_ptr in main. Therefore, data_ptr in main remains NULL, and dereferencing it causes a crash.


Broader Context: C++ and Performance

Your familiarity with C++ makes this distinction even more interesting. C++ has true pass-by-reference syntax, using the & symbol in the function signature: void swap(int& a, int& b). Under the hood, the compiler often implements this using pointers, but it provides a cleaner syntax and stronger guarantees (e.g., references cannot be null).

Understanding these different parameter-passing styles allows you to make conscious design decisions based on performance and API clarity.

  • Pass-by-Value: Best for small, fundamental types (int, char, double). The value can often be passed directly in a CPU register, avoiding memory access entirely. It's the most efficient choice for small data.
  • Pass-by-Pointer (C) / Pass-by-Reference (C++): Best for large structs or objects. This avoids a costly copy of the entire object onto the stack. In C, using a pointer also clearly signals that the argument might be optional (by passing NULL) or that the function will modify the pointed-to data.

In your work with AUTOSAR, when defining a function that operates on a large PDU (Protocol Data Unit) or a complex configuration structure, passing a pointer (const MyStruct* for read-only access, MyStruct* for modifiable access) is standard practice for exactly these performance reasons.


Conclusion

Today we've added critical precision to our understanding of how functions handle arguments in C. While the practical outcome is the same as last lesson, the conceptual clarity is new.

Key Takeaways:

  • Pass-by-Value: C's one and only argument-passing mechanism. The function gets a copy of the argument's value.
  • Pass-by-Reference: A concept that C simulates using pointers, but does not have natively. It involves creating an alias for a variable.
  • The C Method (Pass-by-Pointer): We pass a pointer's value (an address) by value. The function receives a copy of the address, which it can use to access and modify the original data.
  • Practical Implication: A function cannot change the original pointer variable passed to it, only the data it points to.

In our next lesson, we will explore the other side of this interaction: returning pointers from functions. This will bring our understanding of stack frames and variable lifetimes to the forefront as we uncover one of C's most notorious pitfalls: returning a pointer to a local variable that no longer exists. Mastering this is essential for writing safe, stable code that meets functional safety standards.