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.
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:
- Address-of operator (
&): Used in the calling function to get the memory address of a variable.swap(&a, &b); - 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.

Now, let's see how this fixes our swap function.
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.
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
- In
main, aCAN_Message_tvariable namedheartbeat_msgis declared on the stack.memsetinitializes its memory to all zeros. - The address of
heartbeat_msgis passed to thecreate_can_messagefunction. The function's parametermsgnow holds the memory address ofheartbeat_msg. - Inside
create_can_message, themsgpointer is notNULL, so the function proceeds. - The expression
msg->id = 0x1A5;uses the arrow operator to dereference themsgpointer and access theidmember. It writes the value0x1A5directly into theidfield of the originalheartbeat_msgstruct inmain. - The same happens for
dlcand thedataarray members. The function is directly modifying theheartbeat_msgvariable that exists inmain's scope. - When the function returns, the
printfstatements inmaindisplay the new values that were set bycreate_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.