top of page

& in C

In the C programming language, few symbols are as small yet as conceptually dense as &.


At first glance it looks like a simple operator, but the ampersand appears in several completely different contexts:

  • as the address-of operator to obtain the memory address of a variable;

  • as the bitwise AND operator between integer values;

  • as part of &&, the logical AND used in boolean expressions;

  • inside common patterns involving pointers, simulated pass-by-reference, memory‑mapped registers, and bit masking.


Understanding & properly means understanding a core part of C's mental model: memory, bits, and expression semantics.


In this article we perform a complete deep dive into every variant of & in C, with practical examples, common pitfalls, and real-world embedded programming patterns.

Why & Matters So Much in C


C is a language very close to the machine model. Because of that, a single symbol can represent operations that exist at very different abstraction levels.


For &, the three primary meanings are:

  1. obtain a memory address

  2. combine values at the bit level

  3. evaluate logical conjunction (with &&)


These correspond to three conceptual layers of C:

Level

Operator usage

Memory

&x

Bits

a & b

Logic

a && b

This layering makes the ampersand an excellent topic for anyone who wants to deepen their understanding of C.

1. & as the Address-of Operator


The most well-known use of & is obtaining the memory address of an object.

int x = 42;
int *p = &x;

Here:

  • x is an int

  • &x means "the address of x"

  • p is a pointer to int


If x resides at address 0x20000010, then &x literally evaluates to that address.


Example

#include <stdio.h>

int main(void)
{
    int x = 42;
    int *p = &x;

    printf("x = %d\n", x);
    printf("&x = %p\n", (void *)&x);
    printf("p = %p\n", (void *)p);
    printf("*p = %d\n", *p);

    return 0;
}

Key observations:

  • p contains the same address as &x

  • *p dereferences the pointer

Simulating Pass-by-Reference


C uses pass-by-value only, but passing an address allows functions to modify caller variables.

void increment(int *value)
{
    (*value)++;
}

int main(void)
{
    int counter = 10;

    increment(&counter);

    printf("%d\n", counter);
}

his pattern is extremely common in:

  • library APIs

  • embedded drivers

  • buffer management


Example with multiple outputs

void min_max(int a, int b, int *min, int *max)
{
    if (a < b)
    {
        *min = a;
        *max = b;
    }
    else
    {
        *min = b;
        *max = a;
    }
}

min_max(12, 7, &low, &high);

& and Arrays


Arrays introduce subtle but important distinctions.

int arr[5] = {1,2,3,4,5};

Consider the following:


Expression

Type

arr

int* (decays to pointer)

&arr[0]

int*

&arr

int(*)[5]

Even if the printed addresses appear identical, the types are different, and that affects pointer arithmetic.

int *p1 = arr;
int (*p2)[5] = &arr;
  • p1 + 1 moves by sizeof(int)

  • p2 + 1 moves by sizeof(arr)


Understanding this difference is critical when dealing with pointer arithmetic or multidimensional arrays.

& with Structures


Taking the address of structures is very common.

struct Point
{
    int x;
    int y;
};

struct Point p = {10,20};
struct Point *ptr = &p;

You can then use the arrow operator:

ptr->x = 15;
ptr->y = 25;

which is equivalent to:

(*ptr).x = 15;

& and scanf


Many programmers encounter & early with scanf.

int value;
scanf("%d", &value);

scanf must write into the variable, therefore it requires its address.


Incorrect usage:

scanf("%d", value);

This passes an integer instead of a pointer and causes undefined behavior.


Exception with character arrays

char name[20];
scanf("%19s", name);

Here name already decays to char *.

2. & as Bitwise AND


The second meaning of & is bitwise AND.

unsigned a = 0b1101;
unsigned b = 0b1011;

unsigned c = a & b;

Result (bit by bit):

1101
1011
----
1001

Bitwise AND is fundamental in:

  • bit masking

  • flag checking

  • hardware register manipulation

  • protocol parsing

  • packed binary formats


Checking Flags

#define RX_READY (1u << 0)
#define TX_READY (1u << 1)
#define OVERRUN  (1u << 2)

if (status & OVERRUN)
{
    // error detected
}

Embedded Example: Hardware Register

#define UART_SR (*(volatile unsigned int *)0x40001000u)
#define UART_RXNE (1u << 5)

if (UART_SR & UART_RXNE)
{
    // data available
}

Clearing bit

value &= ~(1u << 3);

Extracting Bit Fields

unsigned reg = 0xAB;
unsigned low = reg & 0x0F;

/*
10101011
00001111
--------
00001011 (0x0B)
*/

3. && Logical AND

if ((x > 0) && (y > 0))
{
    // both positive
}

Short-Circuit Evaluation

if ((ptr != NULL) && (ptr->value > 0))

The second expression executes only if the first is true.


Using & instead would be dangerous.


Operator Precedence Trap

if (flags & MASK == 0)

Actually interpreted as:

flags & (MASK == 0)

Correct form:

if ((flags & MASK) == 0)

Rule of thumb:

always use parentheses when checking bit masks.

4. Compound Operator &=

x &= mask;

Equivalent to:

x = x & mask;

Example:

unsigned config = 0xFF;
config &= 0x0F;

5. Real Embedded Patterns


Checking a GPIO input

if ((GPIO_PORT->IDR & PIN3_MASK) != 0)
{
    // pin high
}

Checking multiple error flags

#define ERRORS_MASK (CRC_ERR | FRAME_ERR | OVERRUN_ERR)

if ((status & ERRORS_MASK) != 0)
{
    // error detected
}

Verifying all flags

if ((status & REQUIRED_FLAGS) == REQUIRED_FLAGS)
{
    // all flags active
}

Address-of and Bitwise in the Same Code

typedef struct
{
    volatile unsigned int CR;
    volatile unsigned int SR;
    volatile unsigned int DR;

} UartRegs;

#define UART0 ((UartRegs *)0x40001000u)

UartRegs *uart = UART0;

volatile unsigned int *status_reg = &uart->SR;

Later:

if (uart->SR & UART_RXNE)

Same symbol, two completely different meanings.

Common Mistakes


Forgetting &

foo(x);

when function expects:

foo(&x);

Confusing value vs address

int x = 10;
int *p = &x;
  • x value

  • &x address

  • p pointer

  • *p dereferenced value

Conclusion


The ampersand in C is far more than a symbol. It connects several fundamental layers of the language:

  • memory through the address-of operator

  • bit manipulation through bitwise AND

  • control flow logic through &&


Mastering the differences between these meanings is essential for writing robust C code, especially in areas such as:

  • embedded systems

  • firmware

  • device drivers

  • low-level protocols

  • systems programming


The key takeaway is simple:

In C, & is never just a character. Its meaning depends entirely on context.

And learning to interpret that context precisely is what separates someone who uses C from someone who truly masters it.

Commenti


bottom of page