Need Help or More Information?
Questions about this free C programming course, embedded C training or career guidance? Get in touch.
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 + 1moves bysizeof(*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 type | Typical size of target | Effect of p + 1 | Typical use in embedded code |
|---|---|---|---|
char * / uint8_t * | 1 byte | moves 1 byte | byte buffers, UART frames, register dumps |
uint16_t * | 2 bytes | moves 2 bytes | audio samples, ADC results |
int * / uint32_t * | 4 bytes on Cortex-M and x86-64 | moves 4 bytes | word-sized registers, counters |
double * | 8 bytes | moves 8 bytes | sensor 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.
| Index | Address | Value stored | Expression that reaches it |
|---|---|---|---|
| 0 | 0x2000 | 110 | p or *p |
| 1 | 0x2004 | 220 | p + 1 or *(p + 1) |
| 2 | 0x2008 | 330 | p + 2 or *(p + 2) |
| 3 | 0x200C | 440 | p + 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.
| Form | Pointer is changed | Value of the expression |
|---|---|---|
p++ | after the value is taken | the old address |
++p | before the value is taken | the new address |
p-- | after the value is taken | the old address |
--p | before the value is taken | the 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 achar *is 1 byte and of adouble *is 8 bytes. p++starts at index 0. The expression hands back the old address (index 0, value 110) and only then doespmove to index 1.++pmoves first, sopgoes 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.--psteps back first, so both the result andpland on 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.
| Operation | Legal? | Reason |
|---|---|---|
| pointer + integer, pointer – integer | Yes | moves by whole elements |
| pointer – pointer (same array) | Yes | counts elements between them |
| pointer + pointer | No | the sum of two addresses means nothing |
| pointer * integer, pointer / integer | No | scaling an address has no meaning |
| pointer + float or double | No | an 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 - startwithout 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
%dinstead 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 anduintptr_tfor numeric address checks. - Prefer an
endpointer over a separate length when looping, and compare with<. - Print differences with
%tdand 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 + nmoves byn * 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