Skip to main content

Welcome to a Grasp lesson

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

Lesson 10

Modifying Variables with Pass-by-Pointer

Recommended resources

Hello! Welcome to your next lesson.

In our previous session, we examined the common pitfalls of C pointers: wild, NULL, and dangling pointers. Understanding these helps you write safer, more predictable code by managing pointer states correctly. Today, we'll shift from avoiding errors to actively using pointers for a powerful and fundamental C programming idiom.

The goal for this lesson is to master how to use pass-by-pointer to modify variables in a calling function. This technique is the standard way to overcome C's default "pass-by-value" mechanism and is essential for tasks like writing drivers that configure hardware state or implementing functions that need to return multiple results—scenarios you likely encounter frequently in your work with AUTOSAR and embedded systems.

1. The Default: C's Pass-by-Value Mechanism

As you know, by default, C passes arguments to functions by value. This means the function receives a copy of the arguments, not the original variables themselves. Any modification to these copies inside the function has no effect on the variables in the calling scope.

Let's review a classic example that demonstrates this limitation.

Pointers | Introduction to C

This resource provides a clear example of pass-by-value using a swap function that doesn't work as intended. Pay close attention to the walkthrough table, which visualizes why the changes are isolated within the function.

Read the section titled 'Pass-by-Value'. Focus on the internal_swap.c example and trace the variables a and b from main() into the internal_swap() function using the 'Walkthrough Table'. Notice that the addresses of the variables inside internal_swap are different from those in main.

As the walkthrough demonstrates, internal_swap operates on its own local copies of a and b. Once the function returns, its stack frame is destroyed, and the changes are lost. The original a and b in main remain untouched.


2. The Solution: Pass-by-Pointer

To actually modify the original variables, we need to tell the function where they are located in memory. We achieve this by passing their addresses. This is called pass-by-pointer (or pass-by-address).

The mechanics involve two key operators:

  1. Address-of operator (&): Used in the calling function to get the memory address of a variable. swap(&a, &b);
  2. Dereference operator (*): Used inside the called function to access the value at the address held by a pointer. *p = 100;

This diagram illustrates the core concept: the pointer variable ptr doesn't hold the value 100 but rather the address 0x123 where 100 is stored.

Pointer and Variable Memory Relationship
A pointer variable (`ptr`) stores the memory address of another variable (`Var1`). By dereferencing the pointer, a function can access and modify the value at that address.

Now, let's see how this fixes our swap function.

Pointers | Introduction to C

The following section demonstrates the correct implementation of swap using pointers. It's a direct solution to the problem we just discussed.

Read the section 'Pass-by-Address'. Again, use the 'Walkthrough Table' to trace the execution. Notice that the pointers p and q hold the addresses of main's a and b. When the code executes *p = *q;, it's directly modifying the value at the original address.

By passing addresses, the swap function now has the power to reach back into main's stack frame and alter the values of its variables.


3. Practical Use Case: "Returning" Multiple Values

A C function can only have one return value via the return statement. So, what if you need to get multiple results from a single function call? This is a very common requirement, for instance, in a function that parses a CAN frame and needs to provide the ID, DLC, and payload.

Pass-by-pointer is the idiomatic C solution. You pass the function addresses of several variables from the caller's scope, and the function populates them by dereferencing the pointers it receives.

Pointers | Introduction to C

This example shows how a function can 'return' multiple values by accepting pointers as arguments. It's a practical demonstration that moves beyond the simple swap example.

Read the section 'Multiple Return Values' and study the day_to_dm function. Observe how main declares d and m, passes their addresses (&d, &m) to the function, and day_to_dm then populates them using *d and *m.


4. Application to Structures

The pass-by-pointer technique is especially powerful when working with structs. Passing a large struct by value would involve copying its entire contents onto the stack, which is inefficient. Passing a pointer to the struct is much faster, as only the address (typically 4 or 8 bytes) is copied.

When a function receives a pointer to a struct, it can modify the original struct's members. As you know, the arrow operator (->) is the convenient syntax for dereferencing a struct pointer and accessing a member.

ptr_to_struct->member is syntactic sugar for (*ptr_to_struct).member.

Let's see this in action.

Pointers and Structures in C - Scaler Topics

This resource demonstrates passing a pointer to a struct to a function, allowing the function to modify the original struct's members.

First, briefly review 'Example 2: Accessing structure members using the arrow operator' for a refresher on the -> syntax. Then, focus on 'Example 3: Structure pointer in function arguments'. Note the key sentence: 'Because reference of variable is passed to the function any changes made on structure members inside function body will persist outside the function scope.'


5. Code Analysis Challenge

Let's apply this to a scenario from your domain. Consider the following code for populating a CAN message. Explain what happens when main calls create_can_message.




#include <stdio.h>
#include <stdint.h>
#include <string.h>

// A typical CAN message structure
typedef struct {
    uint32_t id;      // Message ID
    uint8_t  dlc;     // Data Length Code (0-8)
    uint8_t  data[8]; // Payload
} CAN_Message_t;

/**
 * @brief Populates a CAN message structure for a status heartbeat.
 * 
 * @param msg A pointer to the CAN_Message_t struct to be populated.
 */
void create_can_message(CAN_Message_t *msg) {
    if (msg == NULL) {
        return; // Defensive check
    }

    msg->id = 0x1A5; // Set the message ID
    msg->dlc = 4;      // Set the data length
    
    // Set the payload
    msg->data[0] = 0xDE;
    msg->data[1] = 0xAD;
    msg->data[2] = 0xBE;
    msg->data[3] = 0xEF;
}

int main(void) {
    CAN_Message_t heartbeat_msg;
    // It's good practice to initialize the struct
    memset(&heartbeat_msg, 0, sizeof(CAN_Message_t));

    // Pass the address of the struct to the function
    create_can_message(&heartbeat_msg);

    printf("CAN Message ID: 0x%X\n", heartbeat_msg.id);
    printf("DLC: %u\n", heartbeat_msg.dlc);
    printf("Data: %02X %02X %02X %02X\n", 
           heartbeat_msg.data[0], heartbeat_msg.data[1], 
           heartbeat_msg.data[2], heartbeat_msg.data[3]);

    return 0;
}
Click to reveal the analysis
  1. In main, a CAN_Message_t variable named heartbeat_msg is declared on the stack. memset initializes its memory to all zeros.
  2. The address of heartbeat_msg is passed to the create_can_message function. The function's parameter msg now holds the memory address of heartbeat_msg.
  3. Inside create_can_message, the msg pointer is not NULL, so the function proceeds.
  4. The expression msg->id = 0x1A5; uses the arrow operator to dereference the msg pointer and access the id member. It writes the value 0x1A5 directly into the id field of the original heartbeat_msg struct in main.
  5. The same happens for dlc and the data array members. The function is directly modifying the heartbeat_msg variable that exists in main's scope.
  6. When the function returns, the printf statements in main display the new values that were set by create_can_message, demonstrating that the modification was successful.

Conclusion

You've now solidified your understanding of one of C's most fundamental idioms. By mastering pass-by-pointer, you can write efficient and powerful functions that can directly manipulate data structures, configure state, and communicate complex results back to a caller.

Key Takeaways:

  • C's default is pass-by-value, which gives a function a private copy of an argument.
  • Pass-by-pointer is the solution for modifying a caller's data. It involves passing an address (&) and dereferencing a pointer (* or ->).
  • This technique is the standard way to have a function "return" multiple values.
  • It is also more efficient for passing large data structures like structs, as it avoids costly copies.

In our next lesson, we'll add another layer of control and safety to this technique. We will explore how to apply const to pointers, distinguishing between a constant pointer, a pointer to a constant, and a constant pointer to a constant. This will allow you to pass data by pointer for efficiency while guaranteeing that the function cannot accidentally modify it—a crucial concept for robust API design.