Skip to main content

Welcome to a Grasp lesson

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

Lesson 11

Returning Multiple Values with Pointers

Recommended resources

Welcome to the next lesson in our exploration of pointers. In our previous session, we established a crucial technique: using a pointer to modify a single variable that lives inside a calling function. This "pass-by-reference" method is fundamental to C programming.

Today, we will build directly upon that foundation. You'll learn how to extend the same principle to solve one of C's classic limitations: the fact that a function can only have one return value. We will use pointer parameters to allow a single function to "return" multiple distinct results, a technique you'll find indispensable for writing clean and efficient game logic. By the end of this lesson, you will be able to design and implement functions that report several outcomes—like a character's new coordinates and status—in a single call.

The Problem: One Return Value Isn't Always Enough

Many real-world problems produce more than one result. Consider a function for integer division; the operation yields both a quotient and a remainder. Or, in a game context, a function that processes a player's move might need to update their X and Y coordinates simultaneously. The return statement in C, however, can only send back a single value.

While you could write separate functions for each result (e.g., getQuotient() and getRemainder()), this is often inefficient, especially if the underlying calculation is complex. The ideal solution is to perform the calculation once and get all the results back.


The Solution: Output Parameters

The solution is a logical extension of the pass-by-reference technique you've already learned. Instead of using pointer parameters to modify a single input/output variable (like playerHealth), we will define function parameters that serve purely as "output slots."

The calling function will declare local variables to hold the results and then pass the addresses of these variables to the worker function. The worker function, now armed with these addresses, can write its results directly into the caller's variables.

Let's begin with a video that demonstrates this concept clearly.

How To "Return" More Than One Value From A Function | C Programming Example

This video from Portfolio Courses provides an excellent walkthrough, starting from the limitation of a single return value and building up to a complete solution using pointers. It uses the practical example of calculating a rectangle's area and perimeter in one function call.

Please watch the entire video. As you watch, focus on these key steps: The initial area function that demonstrates the standard, single-return approach. How the new function rect is declared with a void return type and two pointer parameters, int *area and int *perimeter (the function signature). How variables are declared in main and their addresses are passed using the & operator (the function call). How the * (dereference) operator is used inside the rect function to write the calculated results to those addresses (the implementation).

This pattern of using pointer parameters to receive output is a cornerstone of C programming. You'll see it used extensively in standard library functions and operating system APIs.


A Formal Example: Division and Remainder

Now that you've seen the general pattern, let's examine it in a more formal context with another classic example: integer division.

Module 4: Pointers

This document from the University of Waterloo provides a concise, academic explanation of this technique.

Please read the section titled Returning more than one value. Pay close attention to the divide function example.

Let's break down the code from the reading.

The function signature is:
void divide(int num, int denom, int *quot, int *rem)

  • num and denom are regular input parameters, passed by value.
  • quot and rem are output parameters. They are pointers that will hold the addresses of the variables where the results should be stored.

The function is called like this:
divide(13, 5, &q, &r);

  • Here, q and r are integer variables declared in the calling function (e.g., main). We pass their addresses to divide.

Inside the function, the assignments are:
*quot = num / denom;
*rem = num % denom;

  • This reads as: "Calculate num / denom, then store the result at the memory location pointed to by quot." The same logic applies to the remainder.
This diagram illustrates a generic stack frame. In our `divide` example, the local variables `q` and `r` reside in the caller's stack frame. The `divide` function receives their addresses, allowing it to reach 'down' the stack and modify them directly, even though they are in a different scope.

Advanced Techniques and Design Considerations

With the basic mechanism understood, we can explore some more sophisticated applications of this pattern.

1. Using the Return Value for Error Codes

Since the function's results are now handled by output parameters, the actual return value is free to be used for another purpose. A very common C idiom is to use the return value to signal the function's status, such as success or an error.

The divide function is a perfect candidate for this. What happens if denom is zero? Division by zero is undefined and would crash the program. We can make the function more robust by checking for this case and returning an error code.

The Waterloo document you just read suggests this exact improvement:

bool divide(int num, int denom, int *quot, int *rem) {
    if (denom == 0) {
        return true; // Return 'true' to indicate an error occurred
    }
    
    // Only perform the calculation and modify the output
    // parameters if the inputs are valid.
    *quot = num / denom;
    *rem = num % denom;
    
    return false; // Return 'false' to indicate success
}

Now, the calling code can check the return value to handle errors gracefully:

int q, r;
if (divide(10, 0, &q, &r)) {
    printf("Error: Cannot divide by zero.\n");
} else {
    printf("Quotient: %d, Remainder: %d\n", q, r);
}

2. Optional Output Parameters

Sometimes, a caller might only be interested in some of the results a function produces. You can design your function to handle this by checking if the pointer passed for an output parameter is NULL.

The following video discusses this technique and also touches upon an alternative method using structs, which we will explore in a later module.

How to Return Two Things from a C or C++ Function

This video from Jacob Sorber offers excellent insights into the design trade-offs of this pattern and demonstrates how to make output parameters optional.

Watch the segment on returning values through arguments. Focus on the if (pman != NULL) check, which allows the caller to pass NULL for a result they don't need.

This NULL check is a hallmark of robust, flexible API design in C.


Application: A Game-Themed Example

Let's synthesize these ideas into a function for your game project. Imagine a function that calculates the outcome of a ranged attack. It needs to "return" two things: the final damage dealt (after accounting for armor) and whether the attack was a critical hit. We'll use the bool return value to indicate if the attack was even possible (e.g., the target was in range).




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

/**
 * Calculates the damage from a ranged attack.
 *
 * @param baseDamage The base damage of the weapon.
 * @param targetArmor The armor value of the target.
 * @param targetDistance The distance to the target.
 * @param weaponRange The maximum range of the weapon.
 * @param finalDamage   OUTPUT: Pointer to store the calculated damage. Cannot be NULL.
 * @param wasCritical   OUTPUT: Pointer to store whether the hit was critical. Can be NULL.
 * @return              'true' on success, 'false' if target is out of range.
 */
bool calculateAttack(int baseDamage, int targetArmor, int targetDistance, int weaponRange, 
                     int *finalDamage, bool *wasCritical) {
    
    // 1. Pre-condition check (as discussed in the video)
    // We require finalDamage, so we ensure the pointer is not NULL.
    if (finalDamage == NULL) {
        // This is a programming error, but we handle it safely.
        return false; 
    }

    // 2. Use the return value for an operational error
    if (targetDistance > weaponRange) {
        *finalDamage = 0; // Ensure output is in a known state
        if (wasCritical != NULL) *wasCritical = false;
        return false; // Attack fails because target is out of range
    }

    // --- Perform Calculations ---
    int damage = baseDamage - targetArmor;
    if (damage < 1) damage = 1; // Always do at least 1 damage

    // Check for a critical hit (e.g., 10% chance)
    bool critical = (rand() % 10) == 0;
    
    if (critical) {
        damage *= 2; // Double damage on a critical hit
    }

    // 3. Write to output parameters
    *finalDamage = damage;
    
    // 4. Handle optional output parameter
    if (wasCritical != NULL) {
        *wasCritical = critical;
    }
    
    return true; // Attack was successful
}

int main() {
    srand(time(NULL)); // Seed the random number generator

    int damageDealt;
    bool isCrit;

    printf("--- Scenario 1: Close-range shot ---\n");
    if (calculateAttack(50, 10, 5, 20, &damageDealt, &isCrit)) {
        printf("Attack succeeded!\n");
        printf("Damage dealt: %d\n", damageDealt);
        printf("Was it a critical hit? %s\n", isCrit ? "YES" : "NO");
    }

    printf("\n--- Scenario 2: Out of range ---\n");
    if (!calculateAttack(50, 10, 25, 20, &damageDealt, &isCrit)) {
        printf("Attack failed: Target is out of range.\n");
    }

    printf("\n--- Scenario 3: Caller only needs damage ---\n");
    if (calculateAttack(50, 10, 5, 20, &damageDealt, NULL)) { // Pass NULL for the optional output
        printf("Attack succeeded! Damage dealt: %d (critical status ignored).\n", damageDealt);
    }
    
    return 0;
}

This example combines all the techniques we've discussed: multiple output parameters, a boolean return for status, and an optional output parameter.


Conclusion

In this lesson, we transformed a limitation of the C language into a powerful and flexible programming pattern. By using pointer parameters as "output slots," we can create functions that efficiently report multiple results, while reserving the function's actual return value for vital status and error checking.

Key Takeaways:

  • The Pattern: Functions that need to produce multiple results can be defined with a void or status-indicating return type (like bool or int), and additional parameters that are pointers to the types of the data you wish to "return."
  • The Call: When calling such a function, you pass the memory addresses (&) of local variables that will receive the output.
  • The Implementation: Inside the function, you dereference the pointer (*) to write the computed result directly into the caller's variable.
  • Robust Design: Good C practice involves using the function's return value for error codes and checking for NULL pointers to support optional output parameters.

In our next lesson, we will make these abstract memory operations concrete. You'll learn how to use the GDB debugger to step through your code line by line, inspect memory addresses, and visually confirm how a function is modifying variables that exist in a completely different stack frame.