Skip to main content

Welcome to a Grasp lesson

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

Lesson 27

Passing Structs: Value vs. Pointer

Recommended resources

Introduction

In our previous lesson, you mastered the distinction between direct and pointer-based access to struct members, learning when to use the dot (.) versus the arrow (->) operator. This was a crucial step in preparing to handle complex data objects in C.

Today, we will address a fundamental design choice you'll face constantly: how to get those structs into your functions. You'll learn to pass structs both by value and by pointer, and more importantly, you will understand the critical trade-offs between these two methods. This lesson is about making informed decisions regarding performance, memory usage, and data integrity—skills that separate a novice from a proficient C programmer. By the end, you'll know not just how to pass structs, but why you should choose one method over the other in different scenarios.


Pass-by-Value: The Simple but Costly Approach

The most straightforward way to pass a struct to a function is "by value." When you do this, the program creates a complete, independent copy of the entire struct and gives it to the function. The function then works with this local copy.

This method has one significant advantage: safety. Since the function has its own copy, any modifications it makes are completely isolated. The original struct in the calling code remains untouched.

However, this safety comes at a cost, particularly for the kinds of applications you're interested in building. To understand why, let's explore the Wikibooks RPG example again.

Learning C With Game Concepts/Designing A Roleplaying ...

This resource provides a clear example of passing a struct by value in a game context.

Focus on the first code example in the article under "Player Implementation". Examine the function prototype void DisplayStats and its definition. Notice that the Target parameter is a struct playerStructure, not a pointer. This means every time DisplayStats is called, the entire Hero struct is copied.

While this works perfectly for a simple display function, imagine a playerStructure with dozens of fields: inventory arrays, skill trees, status effects, etc. The cost of copying this large block of memory every time a function is called can become significant.

The two main downsides are:

  1. Performance: Copying large amounts of data takes CPU time.
  2. Memory: Each function call places a new copy of the struct onto the stack. For large structs or deep function call chains (e.g., function A calls B, which calls C), this can consume substantial stack space and, in extreme cases, lead to a stack overflow.

This is a particularly important consideration in embedded systems, but it's a principle of efficient programming in any context.


Pass-by-Pointer: The Efficient and Powerful Standard

The more common and idiomatic way to handle structs in C is to pass them "by pointer." Instead of copying the entire struct, you simply pass its memory address. The function receives a pointer telling it where to find the original data.

This approach effectively flips the trade-offs of pass-by-value:

  • Pro: High Performance. Only the address (a pointer, typically 4 or 8 bytes) is copied, regardless of how large the struct is. This is incredibly fast and efficient.
  • Pro: Low Memory Usage. No large copy is placed on the stack.
  • Pro/Con: Ability to Modify. The function can now directly modify the original struct's data. This is powerful and often necessary—for example, a function that applies damage to a player needs to change the original player's health. However, it also means you must be careful not to modify data unintentionally.

Let's visualize this. The following image shows a function displayDetail that receives a pointer ptr to an array of student structs. When the calling code passes std to the function, what's actually being passed is the starting address of the array, 1000.

A pointer `ptr` holds the memory address `1000`. This address points to the start of an array of `student` structs in memory. The function `displayDetail` uses this pointer to access the data without needing a copy of the entire array.

This is exactly how we can efficiently handle collections of game entities. A great video resource walks through this comparison and demonstrates the syntax.

How to return and pass a struct to a function

The video "How to return and pass a struct to a function" from CodeVault clearly demonstrates the concepts and their implications.

First, watch the section on passing by value to see the copy in action. The narrator then immediately questions its efficiency. Next, watch the segment on passing by pointer. Pay close attention to how the function signature changes to accept a pointer (struct Point*) and how the arrow operator (->) is now used for access.

The const Keyword: Efficiency with Safety

What if you want the efficiency of passing by pointer but the safety of pass-by-value? C provides the const keyword for this exact purpose. By declaring the pointer parameter as const, you are telling the compiler that the function is only allowed to read from the struct, not write to it.

// This function PROMISES not to change the player data.
void displayPlayerStats(const Player* player) {
    printf("Name: %s\n", player->name); // OK: Reading is allowed.
    // player->score = 100; // ERROR: This would fail to compile.
}

This is a powerful best practice. It gives you the best of both worlds and makes your code's intent much clearer.


Handling Return Values: The "Output Parameter" Idiom

The trade-offs also apply to returning structs from functions. You can return a struct by value, but again, this involves a potentially expensive copy.

A common pitfall is to return a pointer to a struct that was created as a local variable inside the function. This is a serious bug, because the local variable's memory on the stack is freed as soon as the function returns, leaving you with a "dangling pointer" to invalid memory.

The idiomatic C solution is to use an "output parameter." The calling code creates a struct variable and then passes a pointer to it into the function. The function's job is to fill in the members of that struct. The function's return type is often void or an integer used for error reporting.

This pattern is extremely efficient as it avoids all extra copies.

How to return and pass a struct to a function

The same CodeVault video covers these advanced patterns perfectly.

First, watch the short segment explaining the danger of returning a pointer to stack memory. This is a classic C error you must learn to avoid. Then, watch the solution: the output parameter pattern. Notice how the function signature changes to void and accepts an out pointer, which it then populates. This is the professional way to handle this situation.


Summary of Trade-offs

The decision of how to pass a struct is a classic engineering trade-off. Here is a summary to guide your decisions:

Method Pros Cons When to Use
Pass-by-Value Data Safety: Original is protected.
Clear Ownership: No confusion about who frees memory.
Performance: Slow for large structs.
Memory: High stack usage.
Inflexible: Can't modify the original.
For very small structs (e.g., a 2D point with x, y) where the copy overhead is negligible and you value immutability.
Pass-by-Pointer Performance: Very fast.
Memory: Low stack usage.
Flexible: Can modify the original data.
Risk: Prone to accidental modification.
Ownership: Can be less clear if memory is dynamically allocated.
The default choice for most structs. It's the idiomatic, efficient way to work in C.
Pass-by-const Pointer Performance & Memory: Same benefits as by-pointer.
Data Safety: Protects original data from modification.
None, really. It combines the benefits of both. When your function only needs to read data from a struct but the struct is too large to copy efficiently.

For a deeper dive into these trade-offs, including how compilers handle things at a low level, the following discussion is excellent. Given your interest in the underlying mechanics, you might find it particularly insightful.

Are there any downsides to passing structs by value in C, rather than ...

This Stack Overflow thread contains excellent answers from experienced C programmers about the real-world implications of this choice.

Read the answer from 'ShadowRanger' about stack space. This directly explains the memory overhead. Then, read the answer from 'Ilya' that discusses memory ownership and the dangers of copying structs that contain pointers here. Finally, read the short answer from 'Darron' that provides the counterpoint: explicit memory ownership.


Conclusion

You now have a robust framework for making one of the most common design decisions in C programming. You understand that while passing structs by value is simple, passing by pointer is the high-performance standard essential for building complex systems like games.

Key Takeaways:

  • Pass-by-value copies the entire struct, making it safe but inefficient for large data.
  • Pass-by-pointer copies only a memory address, making it fast and memory-efficient. It is the standard C idiom.
  • Using a const pointer gives you the performance of pass-by-pointer with the safety of pass-by-value.
  • To "return" a large struct, use the output parameter pattern: pass a pointer to a struct for the function to fill. Avoid returning pointers to local variables.

In our next lesson, we will put this knowledge to immediate use. We'll start designing more complex game entities using nested structs—for example, a Player struct that contains a Weapon struct—and we will rely on these efficient passing techniques to manage them.