Need Help or More Information?
Questions about this free C programming course, embedded C training or career guidance? Get in touch.
Call by Reference in C
C has no built-in reference parameters, yet call by reference in C is one of the most useful tricks you will learn. Pass an address, dereference it inside the function, and the function can edit the caller’s variable. A hands-on free C tutorial for embedded C programming.
What You Will Learn
- How to simulate call by reference in C using the address-of and dereference operators
- What the compiler really passes when you write
&valueat the call site - How to write a correct swap function with pointer parameters
- How to modify two variables from one function call
- Why
(*p)++and*p++behave completely differently - How embedded C programming uses the same idea for register access helpers
Prerequisites
Finish the previous lecture on call by value first. You should be comfortable declaring pointers, assigning an address to a pointer and reading the value through it with *.
How Call by Reference Works in C
Last lecture we saw that a function gets copies of its arguments. That sounds like a dead end if you want a function to change something in the caller. The way out is simple: if the thing you copy is an address, the copy still points at the original variable. The function can follow that address and read or write the original.
So “call by reference” in C is really call by value of a pointer. The caller writes &variable to produce an address. The function declares a pointer parameter such as int *p to receive it. Inside the function, *p means “the variable that p points to”. Two small changes in syntax, one big change in power.
The Recipe in Four Steps
void f(int *p)f(&count)*p to read or write the caller’s variable.Example 1: A Swap Function That Works
Here is the swap that failed in the last lecture, rewritten with pointer parameters.
#include <stdio.h>
static void swap_int(int *a, int *b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
int main(void)
{
int left = 3, right = 9;
printf("before : left = %d, right = %d\n", left, right);
swap_int(&left, &right);
printf("after : left = %d, right = %d\n", left, right);
return 0;
}
Save the file as swap_ref.c, then:
gcc -std=c11 -Wall -Wextra -o swap_ref swap_ref.c
./swap_ref
before : left = 3, right = 9
after : left = 9, right = 3
Inside swap_int() the pointers a and b are copies, but both copies contain the addresses of left and right. Writing through *a and *b therefore changes the real variables.
Example 2: Changing Two Variables at Once
A function can only return one value, but with pointer parameters it can update as many caller variables as you give it addresses for. This game-style helper adds a life and ten coins in one call.
#include <stdio.h>
static void award(int *lives, int *coins)
{
(*lives)++;
*coins += 10;
printf("inside award : *lives = %d, *coins = %d\n", *lives, *coins);
}
int main(void)
{
int lives = 3;
int coins = 40;
printf("before : lives = %d, coins = %d\n", lives, coins);
award(&lives, &coins);
printf("after : lives = %d, coins = %d\n", lives, coins);
return 0;
}
before : lives = 3, coins = 40
inside award : *lives = 4, *coins = 50
after : lives = 4, coins = 50
Compare this with call by value: there the “after” line would have matched the “before” line. Now the caller sees the change because the function worked on the original memory locations.
The Precedence Trap: (*p)++ vs *p++
Warning: The postfix ++ operator binds tighter than the dereference *. So *p++ means “read the value at p, then move the pointer p to the next int”. It does not increment the variable. To add one to the pointed-to variable you must write (*p)++, as in the example above.
| Expression | What changes | Typical use |
|---|---|---|
(*p)++ | The variable p points to | Counters passed by address |
*p++ | The pointer p itself | Walking through an array |
*p += 10 | The variable p points to | Accumulators |
Call by Reference in Embedded C Programming
Microcontroller peripherals are controlled through registers at fixed addresses. Helper functions that set or clear bits in a register are written exactly like the swap above: they receive the register’s address and modify it in place. The program below uses an ordinary variable as a stand-in for a GPIO output register so you can run it on your laptop.
#include <stdio.h>
#include <stdint.h>
#include <stddef.h>
/* A plain variable standing in for a memory-mapped GPIO output register. */
static volatile uint32_t fake_gpio_odr = 0x00000000u;
static void reg_set_bits(volatile uint32_t *reg, uint32_t mask)
{
if (reg == NULL) {
return;
}
*reg |= mask;
}
static void reg_clear_bits(volatile uint32_t *reg, uint32_t mask)
{
if (reg == NULL) {
return;
}
*reg &= ~mask;
}
int main(void)
{
reg_set_bits(&fake_gpio_odr, (1u << 5) | (1u << 2));
printf("after set : ODR = 0x%08X\n", (unsigned int)fake_gpio_odr);
reg_clear_bits(&fake_gpio_odr, 1u << 5);
printf("after clear : ODR = 0x%08X\n", (unsigned int)fake_gpio_odr);
return 0;
}
after set : ODR = 0x00000024
after clear : ODR = 0x00000004
Three details here are worth copying into your own drivers. First, the parameter is volatile uint32_t * so the compiler never caches or removes accesses to a hardware register. Second, the functions test for NULL before dereferencing. Third, the bit mask is passed by value because it is a small, read-only input. This mix, pointer for the thing you modify and plain value for the thing you only read, is the everyday pattern in embedded C.
Where You Will See This in Real Code
Once you recognise the pattern you will spot it everywhere. Library functions such as scanf, memcpy and strtol take addresses so they can write results back. RTOS calls like queue receive functions fill a buffer you pass in. Peripheral drivers accept a pointer to a configuration struct, read it, and update a status field in the same object. All of these are call by reference in C, built from nothing more than pointers and the two operators you already know.
Call by Value vs Simulated Call by Reference
| Feature | Call by value | Call by reference (pointers) |
|---|---|---|
| What is passed | Copy of the data | Copy of the address |
| Parameter type | int x | int *p |
| Call site | f(x) | f(&x) |
| Can modify caller’s variable | No | Yes, through *p |
| Risk | Lost updates | NULL or dangling pointer |
| Best for | Small read-only inputs | In/out data, large objects, registers |
Common Mistakes and Troubleshooting
| Mistake | What happens | Fix |
|---|---|---|
Forgetting & at the call site | Compiler warns about int to pointer conversion, or the program crashes | Compile with -Wall -Wextra and read warnings |
Writing *p++ to increment a value | Pointer moves, value untouched | Use (*p)++ |
Passing NULL or an uninitialised pointer | Crash or hard fault | Check for NULL, initialise pointers |
Swapping with a = b instead of *a = *b | Only the local pointer changes | Dereference before assigning |
| Passing the same variable twice to a swap | Harmless for swap, but surprising for other helpers | Document aliasing assumptions |
Best Practices for Call by Reference in C
- Use
constfor read-only pointers. If a function only reads through a pointer, declare itconst int *p. The compiler then rejects accidental writes. - Check for NULL in library-style functions, especially in firmware where a bad pointer means a hard fault rather than a friendly error.
- Keep small inputs by value. Do not turn every parameter into a pointer. Plain values are easier to read and often faster.
- Performance: a pointer costs one extra memory access per use, but it avoids copying large structures. Choose by size and by whether you must modify the original.
- Security: a function that receives a writable pointer can corrupt any memory that pointer reaches. Pass only the addresses a function needs, and validate sizes whenever a pointer refers to a buffer.
- Name your out-parameters clearly, for example
out_count, so call sites read like documentation.
Key Takeaways
- C always passes by value; call by reference is simulated by passing addresses.
- Use
&when calling and*inside the function. - The pointer parameter is itself a copy, but it still aims at the caller’s original data.
(*p)++changes the pointed-to value,*p++changes the pointer.- Embedded drivers rely on
volatilepointer parameters to touch hardware registers.
Conclusion
Call by reference in C is not a language feature, it is a habit built on pointers, and once it clicks you will see it in every real code base: scanf, memcpy, driver init functions and RTOS APIs all receive addresses so they can fill in results. Practise by writing your own helpers that swap, increment and reset variables through pointers, and always compile with warnings on. Next we use this idea to let one function hand back several results at once, an essential skill in any free embedded C course.
Frequently Asked Questions
Is call by reference in C real or simulated?
Simulated. The address is copied into the pointer parameter, so technically C still uses call by value. The effect, however, is what people call call by reference.
Why does scanf need the address-of operator?
scanf must store the value it reads into your variable. It can only do that if it receives the variable’s address, exactly like the swap function here.
What is the difference between int *p and int &p?
The second form, a C++ reference, does not exist in C. In C you always use pointers.
Can I pass a constant like 5 to a pointer parameter?
No. A pointer parameter needs an address of an object. Store the constant in a variable and pass that variable’s address.
When should I prefer returning a value over using a pointer?
Whenever there is a single result. Returning a value keeps the code simpler and the function easier to test.
Why is volatile used in register helpers?
It tells the compiler that the memory can change outside the program flow, so every read and write must really happen instead of being optimised away.
Master Pointers with Free Lessons
Follow the whole EmbeddedPathashala free embedded C course, from basics to pointers, with tested examples and interview questions. Next up: return multiple values from a function in C.
Need Help or More Information?
Questions about this free C programming course, embedded C training or career guidance? Get in touch.

2 Comments