What is C Pointer Arithmetic Explained – 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

C Pointer Arithmetic Explained

Free C programming course: scaling, prefix and postfix increment, pointer subtraction and illegal operations.

Welcome to this free C programming course lecture on pointer arithmetic in C. Every embedded C programming job reaches the moment where you must walk a buffer, a lookup table or a descriptor ring with nothing but a pointer, and that walk only works if you know exactly how pointers move. In this free C tutorial you will run original programs that show how increment, decrement and subtraction behave, and which pointer operations the compiler refuses.

What You Will Learn

  • Why p + 1 moves by sizeof(*p) bytes instead of one byte
  • The exact difference between p++, ++p, p-- and --p
  • How pointer subtraction counts elements and why its type is ptrdiff_t
  • Which arithmetic operations on pointers are illegal and why
  • How these rules apply to buffers and tables in embedded C programming

Prerequisites

  • Pointer declaration, the address operator and dereferencing from the earlier lectures of this chapter
  • Arrays and indexing from the Arrays chapter
  • A C compiler such as GCC or Clang on Linux, or an embedded toolchain with a host test build

How Pointer Arithmetic in C Is Scaled

When you write p + 1, the compiler does not add one byte to the address. It adds sizeof(*p) bytes. This rule is called scaling, and it is the reason the very same expression walks an array of char, int or double without you ever writing a byte count. The full formula is simple: the new address equals the old address plus n * sizeof(*p).

Because the step depends on the pointed-to type, the declared type of a pointer is not decoration. A char * and an int * holding the same address behave differently the moment you add something to them.

Pointer typeTypical size of targetEffect of p + 1Typical use in embedded code
char * / uint8_t *1 bytemoves 1 bytebyte buffers, UART frames, register dumps
uint16_t *2 bytesmoves 2 bytesaudio samples, ADC results
int * / uint32_t *4 bytes on Cortex-M and x86-64moves 4 bytesword-sized registers, counters
double *8 bytesmoves 8 bytessensor math on hosted targets

Do not trust old textbook sizes

Books written for 16-bit compilers often say an int pointer moves 2 bytes. On the ARM Cortex-M and 64-bit desktop compilers you will use today, int is usually 4 bytes. Always check sizeof on your own target instead of memorising a number.

Pointer p walking an int array (addresses are illustrative)
IndexAddressValue storedExpression that reaches it
00x2000110p or *p
10x2004220p + 1 or *(p + 1)
20x2008330p + 2 or *(p + 2)
30x200C440p + 3 or *(p + 3)

Notice that each address is 4 higher than the previous one, yet the expression only ever adds 1, 2 or 3. The compiler multiplies by the element size for you. This is also why adc[2] and *(adc + 2) mean exactly the same thing.

Pointer Increment and Decrement: Prefix vs Postfix

Incrementing a pointer with ++ moves it to the next element of its type, and decrementing with -- moves it to the previous element. Both operators come in a prefix and a postfix form, and both forms change the pointer by exactly one element. The difference is only in which value the whole expression produces.

FormPointer is changedValue of the expression
p++after the value is takenthe old address
++pbefore the value is takenthe new address
p--after the value is takenthe old address
--pbefore the value is takenthe new address

The program below measures the scaling with real numbers and then runs all four forms on a small array of ADC readings. Type it in, do not paste it blindly; the point of this free C tutorial is that you predict each line before you run it.

#include <stdio.h>
#include <stddef.h>

int main(void)
{
    int    adc[4]  = {110, 220, 330, 440};
    char   tag[4]  = {'A', 'B', 'C', 'D'};
    double volt[3] = {1.1, 2.2, 3.3};

    int    *pi = adc;
    char   *pc = tag;
    double *pd = volt;

    printf("one step of int*    = %td bytes\n", (char *)(pi + 1) - (char *)pi);
    printf("one step of char*   = %td bytes\n", (char *)(pc + 1) - (char *)pc);
    printf("one step of double* = %td bytes\n", (char *)(pd + 1) - (char *)pd);

    int *p = adc;
    int *got;

    got = p++;
    printf("p++ : result at index %td, p now at %td, *got=%d\n", got - adc, p - adc, *got);
    got = ++p;
    printf("++p : result at index %td, p now at %td, *got=%d\n", got - adc, p - adc, *got);
    got = p--;
    printf("p-- : result at index %td, p now at %td, *got=%d\n", got - adc, p - adc, *got);
    got = --p;
    printf("--p : result at index %td, p now at %td, *got=%d\n", got - adc, p - adc, *got);

    printf("*(adc + 2) = %d\n", *(adc + 2));

    int *first = &adc[0];
    int *last  = &adc[3];
    printf("last - first        = %td elements\n", last - first);
    printf("byte gap            = %td bytes\n", (char *)last - (char *)first);
    return 0;
}

File name ptr_step.c. Build and run it like this:

gcc -std=c11 -Wall -Wextra -o ptr_step ptr_step.c
./ptr_step

Expected output on a typical 64-bit Linux machine (an ARM Cortex-M target prints the same):

one step of int*    = 4 bytes
one step of char*   = 1 bytes
one step of double* = 8 bytes
p++ : result at index 0, p now at 1, *got=110
++p : result at index 2, p now at 2, *got=330
p-- : result at index 2, p now at 1, *got=330
--p : result at index 0, p now at 0, *got=110
*(adc + 2) = 330
last - first        = 3 elements
byte gap            = 12 bytes

Reading the output line by line

  • The first three lines prove scaling: one step of an int * is 4 bytes, of a char * is 1 byte and of a double * is 8 bytes.
  • p++ starts at index 0. The expression hands back the old address (index 0, value 110) and only then does p move to index 1.
  • ++p moves first, so p goes from index 1 to index 2 and the expression already carries the new address (value 330).
  • p-- returns the current address (index 2) and then steps back to index 1.
  • --p steps back first, so both the result and p land on index 0.
Pointer movement through the four statements
startp at index 0
after p++p at index 1result was index 0
after ++pp at index 2result was index 2
after p–p at index 1result was index 2
after –pp at index 0result was index 0

Subtracting Two Pointers

Subtracting one pointer from another of the same type does not give a byte difference. It gives the number of elements between them. In the program, last - first is 3 because adc[3] sits three elements after adc[0], while casting both to char * first reveals the real 12-byte gap.

The result type is ptrdiff_t, declared in <stddef.h>, and you print it with the %td conversion. Using %d here is a classic bug on 64-bit systems because the value is wider than an int.

Stay inside one array

Pointer subtraction is only defined when both pointers point into the same array object, or one element past its end. Subtracting pointers to two unrelated variables is undefined behaviour, even if it appears to work on your board today.

Pointer Operations the Compiler Rejects

Only a small set of arithmetic is legal on pointers. The meaningful ones are: pointer plus integer, pointer minus integer, pointer minus pointer, and the increment and decrement operators. Everything else has no sensible meaning, because an address is not an ordinary number.

OperationLegal?Reason
pointer + integer, pointer – integerYesmoves by whole elements
pointer – pointer (same array)Yescounts elements between them
pointer + pointerNothe sum of two addresses means nothing
pointer * integer, pointer / integerNoscaling an address has no meaning
pointer + float or doubleNoan element offset must be a whole number

Here is a tiny file that deliberately breaks every rule in the table. Save it as ptr_illegal.c and compile only (the -c flag stops before linking):

#include <stdio.h>
int main(void)
{
    int v[4] = {1, 2, 3, 4};
    int *a = v, *b = v + 1;
    int *s = a + b;
    int *m = a * 2;
    int *d = a / 2;
    int *f = a + 1.5;
    return 0;
}
gcc -std=c11 -c ptr_illegal.c

GCC refuses all four lines:

bad.c:6:16: error: invalid operands to binary + (have ‘int *’ and ‘int *’)
bad.c:7:16: error: invalid operands to binary * (have ‘int *’ and ‘int’)
bad.c:8:16: error: invalid operands to binary / (have ‘int *’ and ‘int’)
bad.c:9:16: error: invalid operands to binary + (have ‘int *’ and ‘double’)

If you really need the numeric value of an address, for example to check alignment, convert it to uintptr_t first and do integer arithmetic on that, then convert back only when you must.

Where Pointer Arithmetic in C Matters in Embedded Systems

  • Walking a receive buffer byte by byte while parsing a UART or SPI frame.
  • Stepping through a lookup table of sine values or CRC constants.
  • Advancing a head or tail pointer inside a ring buffer filled by an interrupt handler.
  • Moving through a table of memory-mapped peripheral configuration words.
  • Computing the length of a received message as end - start without keeping a separate counter.

A note on void pointers

Standard C does not allow arithmetic on void * because void has no size. GCC allows it as an extension and treats the step as 1 byte, but portable code should cast to uint8_t * first.

Performance and Safety of Pointer Arithmetic in C

On modern compilers, indexing with buf[i] and walking with *p++ usually compile to equivalent machine code, so choose the form that reads best. Pointer walking pays off most when you remove a multiply from a tight loop on a small core without a hardware multiplier, but measure before you trust that claim on your own toolchain.

Safety is the bigger concern. The language lets you form a pointer one element past the end of an array, and you may compare against it, but you must never dereference it. Moving further out is undefined behaviour and is a common source of buffer overruns in firmware that parses data from a network or a radio.

Common Pointer Arithmetic Mistakes

  • Assuming pointer arithmetic in C adds one byte regardless of type.
  • Printing the result of pointer subtraction with %d instead of %td.
  • Subtracting pointers that belong to different arrays.
  • Walking one element past the end and then dereferencing it.
  • Doing arithmetic on void * and assuming it is portable.

Best Practices for Embedded C Programming

  • Remember that pointer arithmetic in C depends on the declared type, so keep pointer type and data type matched.
  • Use uint8_t * for raw byte walking and uintptr_t for numeric address checks.
  • Prefer an end pointer over a separate length when looping, and compare with <.
  • Print differences with %td and sizes with %zu.
  • Test the pointer logic on a host build before running it on hardware.

Key Takeaways

  • Pointer arithmetic in C is scaled: p + n moves by n * sizeof(*p) bytes.
  • Prefix forms yield the new pointer, postfix forms yield the old one; both change the pointer by one element.
  • Pointer minus pointer gives an element count of type ptrdiff_t.
  • Adding two pointers, scaling a pointer, or adding a float to one is a compile error.
  • Never go beyond one element past the end of an array.

Conclusion

Pointer arithmetic in C looks small, but it is the engine behind buffer parsing, table lookups and ring buffers in embedded firmware. Once you accept that the compiler scales every step by the element size, the rest of the rules fall into place: stay inside the array, keep types consistent, and let subtraction count elements for you.

In the next lecture of this free embedded C course we combine the dereference operator with increment and decrement, which is where most students first get confused by expressions like *p++ and ++*p.

Frequently Asked Questions

What is pointer arithmetic in C?

It is the set of operations C allows on pointers: adding or subtracting an integer, subtracting two pointers of the same type, and incrementing or decrementing. Every step is scaled by the size of the pointed-to type.

Why does p + 1 not add one byte?

Because the compiler multiplies the offset by the size of the pointed-to type. For an int pointer on a 32-bit or 64-bit platform that is typically 4 bytes.

What is the difference between p++ and ++p for a pointer?

Both move the pointer one element forward. The postfix form gives you the old address as the value of the expression, the prefix form gives the new address.

What does subtracting two pointers return?

The number of elements between them, with type ptrdiff_t. Both pointers must point into the same array.

Can I add two pointers together?

No. The sum of two addresses has no meaning and the compiler reports an error. Only pointer plus integer and pointer minus pointer are allowed.

Is pointer arithmetic allowed on void pointers?

Not in standard C. GCC accepts it as an extension that steps one byte, but portable embedded code should cast to uint8_t pointer first.

Is this lecture part of a free embedded C course?

Yes. It belongs to the free C programming course on EmbeddedPathashala, built for students who want to learn embedded C programming without paying for training.

Keep Learning Embedded C for Free

Continue with the next lecture of our free C programming course and master dereference and increment together.

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 *