Skip to main content

Welcome to a Grasp lesson

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

Lesson 10

Pointers and Pass-by-Reference in C

Recommended resources

Welcome to our next lesson. In our previous session, we covered the essential mechanics of pointers in C: how to declare them, get a variable's address using the & operator, and access the value at that address by dereferencing with the * operator. We established the "what" and "how" of pointer syntax.

Today, we will immediately put that knowledge to work to solve one of the most common and important challenges in C programming: how to write a function that can modify a variable that belongs to its caller. This technique is known as pass-by-reference, and mastering it is fundamental to writing effective C programs, particularly for tasks like managing game state where many functions need to update a central set of data.

The Problem: C's "Pass-by-Value" Default

Before we see how pointers solve the problem, we must first understand why the problem exists. By default, when you pass an argument to a function in C, the function receives a copy of that argument's value, not the original variable itself. This is called pass-by-value. Any changes the function makes are to its own local copy, which is discarded when the function returns. The original variable in the calling function remains untouched.

Let's watch a short video that illustrates this exact scenario. A programmer named Albert is trying to write a simple increment function, but is surprised when it doesn't work as he expects.

Pointers as function arguments - call by reference

This segment from the mycodeschool channel clearly demonstrates the limitation of C's default pass-by-value behavior.

Watch the section from this point in the video where the narrator describes Albert's problem and shows the code that fails to increment the variable a.

As you saw, the value of a in main remains 10. Why? Because the increment function received only the value 10, stored it in its own local variable (also named a), and incremented that copy to 11. The original a in main was never involved.


Understanding the "Why": The Call Stack

To understand this on a deeper, low-level, you need to visualize how a program uses memory when functions are called. When a function is invoked, a block of memory called a stack frame is created for it on the call stack. This frame holds the function's local variables, including its parameters.

The following video provides an excellent visual explanation of the call stack. This is a critical concept for understanding how programs execute and manage their state in memory.

Pointers as function arguments - call by reference

The same video provides one of the clearest explanations of the call stack and how it leads to the pass-by-value behavior we just observed.

Please watch two parts of this video: First, watch from this segment which confirms that the variables in main and increment have different memory addresses. Then, continue from the explanation of the call stack. Focus on how a new stack frame is created for main, and then another separate frame is created on top of it for the increment function. This visual of separate memory spaces is the key takeaway.

The core idea is that main's a lives in main's stack frame. When increment is called, a new stack frame is created with its own a, and the value from main's a is copied into it. The increment function can only access variables within its own stack frame, so it can't touch the original a. When increment finishes, its stack frame is destroyed, and the modified copy is lost forever.


The Solution: Pass-by-Reference with Pointers

So, if a function can't access a variable in another function's stack frame, how can we ever modify it? The answer is: we don't pass the variable's value; we pass its address.

If the function knows the memory address of the original variable, it can use a pointer to reach back into the caller's stack frame and directly read or write the value at that address. This is the essence of pass-by-reference.

This diagram illustrates the process perfectly. The `main` function passes the address of `q` (`&q`) to the `test` function. The `test` function then uses this address to change the value stored in the original `q` from 20 to 200.

Let's see how Albert's code is fixed using this technique.

Pointers as function arguments - call by reference

This final clip from mycodeschool shows the corrected code and visualizes how passing the address allows the function to modify the original variable.

Watch the section from the solution. Pay close attention to the three key changes: The function signature now accepts a pointer: void increment(int *p). Inside the function, the pointer is dereferenced to modify the value: *p = (*p) + 1;. The function is called with the address of the variable: increment(&a);.


A Clean Code Example

The video provides a great conceptual overview. Now, let's examine a concise, complete C program that you can compile and run. The following reading provides a clear example and explanation.

Demystifying Pointers in C: Understanding Pass-by- ...

This article provides a simple, well-explained code example of pass-by-reference.

Read Example 1 in the article. Focus on the C code block and the explanation that follows. Notice how the author prints the memory addresses from both main and the modifyPointer function to prove that they are indeed working with the same location in memory. You can find this example under the heading Example 1.


Practical Application: A Game-Themed Example

Let's apply this to a more practical scenario related to your game development goals. Imagine you have a player's health stored in a variable in your main game loop. You want to create a separate function, applyDamage, to handle the logic of reducing the player's health.

Here is how you would implement it using pass-by-reference:




#include <stdio.h>

// The function takes a POINTER to an integer as its first parameter.
// This gives it the address of the health variable to modify.
void applyDamage(int *health, int damageAmount) {
    // Dereference the pointer to get the current health value.
    int currentHealth = *health;
    
    // Calculate the new health.
    int newHealth = currentHealth - damageAmount;
    
    // Ensure health doesn't drop below zero.
    if (newHealth < 0) {
        newHealth = 0;
    }
    
    printf("        [applyDamage]: Player takes %d damage. Health changing from %d to %d.\n", 
           damageAmount, currentHealth, newHealth);
           
    // Dereference the pointer AGAIN, this time to WRITE the new value
    // back to the original address.
    *health = newHealth;
}

int main() {
    int playerHealth = 100;
    
    printf("--- Game Start ---\n");
    printf("Initial player health: %d\n\n", playerHealth);
    
    printf("--- First Hit ---\n");
    // Pass the ADDRESS of playerHealth to the function.
    applyDamage(&playerHealth, 30);
    printf("Player health after hit: %d\n\n", playerHealth);
    
    printf("--- Second Hit ---\n");
    applyDamage(&playerHealth, 45);
    printf("Player health after hit: %d\n\n", playerHealth);

    printf("--- Massive Hit ---\n");
    applyDamage(&playerHealth, 50); // This should take health to 0, not negative.
    printf("Final player health: %d\n\n", playerHealth);

    return 0;
}

This pattern is extremely powerful. As you build more complex game logic, you won't be passing simple integers, but pointers to large structs representing the player, enemies, or items. Passing a pointer is vastly more efficient than copying an entire complex data structure into a new stack frame for every function call.

A brief final video reinforces why this is so important for scalability.

you will never ask about pointers again after watching this video

This short clip from the "Low Level" channel explains why pointers are not just a trick, but a necessity for writing clean, scalable code.

Watch the segment from Why use pointers?. The example shows passing a person structure by reference, which is exactly the pattern we will use for game entities later in this course.


Conclusion

In this lesson, we bridged the gap between pointer syntax and practical application. You've learned how to overcome C's default pass-by-value behavior to enable functions to modify their callers' data—a cornerstone of procedural programming.

Key Takeaways:

  • Pass-by-value: C's default behavior where functions receive copies of arguments. Changes are local and temporary.
  • The Call Stack: Function calls create separate stack frames in memory, isolating local variables and preventing direct access between functions.
  • Pass-by-reference: The technique of passing a variable's memory address (a pointer) to a function, giving it the ability to modify the original data.
  • The Implementation Pattern:
    1. Define the function to accept a pointer: void func(int *p);
    2. Call the function with the address of the variable: func(&my_var);
    3. Dereference the pointer inside the function to access the data: *p = 100;

In our next lesson, we'll build directly on this concept. You'll learn how to use this same pointer technique to solve another classic C limitation: how to effectively "return" multiple values from a single function.