What is Call by Reference in C- Best Embedded C Programming Course

Need Help or More Information?

Questions about this free C programming course, embedded C training or career guidance? Get in touch.

Free C Programming Course · Chapter 8 Pointers

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.

3
Tested examples
14
Minutes to read
C11
Standard used

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 &value at 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

Simulating Call by Reference
1
Declare
Give the parameter a pointer type: void f(int *p)
2
Pass
At the call site send the address: f(&count)
3
Dereference
Inside f use *p to read or write the caller’s variable.
4
Return
The pointer copy dies, but the change in the caller’s variable stays.

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.

Pointers a and b Aim at the Caller’s Variables
main() frame
3
left (becomes 9 after the swap)
9
right (becomes 3 after the swap)
swap_int() frame
a holds the address of left
b holds the address of right
tmp is a plain local int
a → leftb → right

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.

ExpressionWhat changesTypical use
(*p)++The variable p points toCounters passed by address
*p++The pointer p itselfWalking through an array
*p += 10The variable p points toAccumulators

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

FeatureCall by valueCall by reference (pointers)
What is passedCopy of the dataCopy of the address
Parameter typeint xint *p
Call sitef(x)f(&x)
Can modify caller’s variableNoYes, through *p
RiskLost updatesNULL or dangling pointer
Best forSmall read-only inputsIn/out data, large objects, registers

Common Mistakes and Troubleshooting

MistakeWhat happensFix
Forgetting & at the call siteCompiler warns about int to pointer conversion, or the program crashesCompile with -Wall -Wextra and read warnings
Writing *p++ to increment a valuePointer moves, value untouchedUse (*p)++
Passing NULL or an uninitialised pointerCrash or hard faultCheck for NULL, initialise pointers
Swapping with a = b instead of *a = *bOnly the local pointer changesDereference before assigning
Passing the same variable twice to a swapHarmless for swap, but surprising for other helpersDocument aliasing assumptions

Best Practices for Call by Reference in C

  • Use const for read-only pointers. If a function only reads through a pointer, declare it const 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 volatile pointer 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

Leave a Reply

Your email address will not be published. Required fields are marked *