Need Help or More Information?
Questions about this free C programming course, embedded C training or career guidance? Get in touch.
Array Parameters Decay Explained
Why an array parameter is really a pointer, why sizeof inside the function disagrees with sizeof outside, and how to sort and scan an array using only pointer arithmetic. Free C tutorial for embedded systems.
When you pass an array to a function, C does not copy the array. It passes the address of the first element, and the parameter you wrote as int a[10] is quietly rewritten by the compiler as int *a. This behaviour is called array decay in C, and it shapes how you write every buffer-handling routine in embedded C programming.
This lecture of our free C programming course works through three classic exercises: assigning to an array parameter, scanning with an offset starting address, and a bubble sort written entirely with pointer arithmetic and a swap helper. All examples are rewritten from scratch and have been compiled and run.
What You Will Learn
- What array decay is and when it does not happen
- Why
int a[10],int a[]andint *aare the same parameter - Why you cannot assign to an array but can assign to an array parameter
- How passing
arr + 3starts a function in the middle of an array - How to sort with
*(base + j)and a pointer swap helper - How to pass array length safely
Prerequisites
You need pointer arithmetic, the relationship a[i] equals *(a + i), and the pointer parameter rules from the previous lecture. A host compiler such as GCC is enough to try every example.
What Is Array Decay in C?
In most expressions, the name of an array is converted into a pointer to its first element. The exceptions are sizeof, the unary &, and string-literal initialisers. A function call is not an exception, so the callee receives a pointer and has no idea how many elements are behind it.
| Parameter written as | Compiler treats it as | Remarks |
|---|---|---|
int a[10] | int *a | The 10 is documentation only |
int a[] | int *a | Most common array-style spelling |
int *a | int *a | Honest spelling of what it is |
int a[static 10] | int *a plus a promise | C99: caller guarantees at least 10 valid elements |
Array Parameter sizeof: The Surprise
Here is the program that proves it. Save it as ep_array_param.c. It also demonstrates assigning to an array parameter, starting a scan at samples + 3, and a pointer-only bubble sort.
#include <stdio.h>
static void report(int buf[10])
{
printf("inside : sizeof buf = %zu\n", sizeof buf);
}
static void rebind(int buf[10])
{
int spare = 5;
buf = &spare; /* legal: buf is just a local pointer */
}
static void print_until(const int *p, int stop)
{
for (int i = 0; p[i] != stop; i++)
printf("%d ", p[i]);
printf("\n");
}
static void swap_int(int *a, int *b)
{
int t = *a;
*a = *b;
*b = t;
}
static void sort_ptr(int *base, int n)
{
for (int i = 0; i < n - 1; i++)
for (int j = 0; j < n - 1 - i; j++)
if (*(base + j) > *(base + j + 1))
swap_int(base + j, base + j + 1);
}
int main(void)
{
int samples[10] = {90, 85, 70, 65, 60, 55, 40, 35, 30, 25};
printf("outside: sizeof samples = %zu\n", sizeof samples);
report(samples);
rebind(samples);
printf("samples[0] after rebind = %d\n", samples[0]);
print_until(samples + 3, 40);
sort_ptr(samples, 10);
for (int i = 0; i < 10; i++)
printf("%d ", samples[i]);
printf("\n");
return 0;
}
Build and Run
$ gcc -Wall -Wextra -std=c11 -o ep_array_param ep_array_param.c
ep_array_param.c: In function 'report':
ep_array_param.c:5:50: warning: 'sizeof' on array function parameter 'buf'
will return size of 'int *' [-Wsizeof-array-argument]
$ ./ep_array_param
outside: sizeof samples = 40
inside : sizeof buf = 8
samples[0] after rebind = 90
65 60 55
25 30 35 40 55 60 65 70 85 90
GCC even warns that sizeof on the parameter returns the size of a pointer. On a 64-bit PC the pointer is 8 bytes. On a 32-bit Cortex-M it prints 4. Old 16-bit DOS compilers printed 2, which is why older textbooks show that number.
Reading the Output Line by Line
- 40 versus 8. Ten ints of 4 bytes each outside, one pointer inside.
- samples[0] stays 90.
rebindpointed its own local pointer at a different variable. The caller’s array never changed. - 65 60 55. We passed
samples + 3, so the scan starts at element 3 (value 65) and stops when it sees 40, the sentinel. - Sorted ascending. The bubble sort works through
basedirectly, and the helper receives addresses of two elements.
Why You Cannot Assign to an Array
The rebind trick works for the parameter because the parameter is a pointer. A real array variable is different. Its name is not a modifiable lvalue, so this tiny file fails:
int main(void)
{
int local_buf[10];
int spare = 4;
local_buf = &spare; /* arrays are not assignable */
return 0;
}
$ gcc -std=c11 -c ep_array_assign_err.c
ep_array_assign_err.c:6:15: error: assignment to expression with array type
Two similar-looking lines, two very different meanings
Inside a function, int buf[10] as a parameter is a pointer you may reassign. int buf[10] as a local variable is a block of memory you may not. The brackets mean different things depending on where they appear.
Passing the Length Along
Because the callee cannot recover the size, every array-handling function needs the count as a separate parameter. The idiom is (const int *data, size_t n). In the caller, compute the count with sizeof arr / sizeof arr[0] while the name is still a real array.
Sentinels, such as stopping at the value 40 in print_until, are fragile. If the sentinel is absent, the loop walks off the end of the array. Use them only for data whose format guarantees a terminator, such as C strings.
Real-World Uses in Embedded Systems
- ADC sample buffers. A filter function takes
(const uint16_t *samples, size_t n)and works the same for a 16-sample and a 1024-sample buffer. - UART and SPI drivers.
uart_write(const uint8_t *data, size_t len)is the standard shape. Passingbuf + offsetlets you send a tail of a buffer without copying. - DMA descriptors. The driver stores a base pointer and a count, never an array.
- Sorting and median filters. A median-of-five filter often uses a small insertion sort over a pointer and a length.
Common Mistakes and Troubleshooting
| Mistake | Result | Fix |
|---|---|---|
Using sizeof on an array parameter | Pointer size, not array size | Pass the element count explicitly |
| Scanning for a sentinel that may be missing | Out-of-bounds read | Loop on index < n, not on a value |
| Assigning to a local array name | Compile error | Copy elements with a loop or memcpy |
| Expecting a function to resize the caller’s array | Impossible, size is fixed at definition | Pass a larger buffer or use dynamic allocation |
| Forgetting that sort changes the caller’s data | Original order lost | Sort a copy if you still need the original |
*(base + j + 1) is only safe because the inner loop stops at n - 1 - i. Change that limit by one and you read past the last element.
Best Practices
- Declare array parameters as pointers, with
constwhen read-only, and always pass a length. - Use
size_tfor lengths and indices. - Compute array length at the definition site with
sizeof arr / sizeof arr[0], ideally inside anARRAY_LENmacro. - Keep swap helpers tiny and pointer-based, so they can be reused for any sort routine.
- Enable
-Wsizeof-array-argument(part of-Wallin GCC) and never ignore it.
Performance and Security Considerations
Passing a pointer plus a length costs one or two registers regardless of array size, so large buffers never get copied on the way in. Bubble sort is O(n squared) and fine for a handful of samples, but use a better algorithm for hundreds. For security, the length must come from a trusted source. A length taken from a received packet without checking is the classic route to buffer overflows.
Suggested Images
| Image idea | ALT text |
|---|---|
| Array in main shrinking to a single pointer in callee | Array parameter decay in C turning an array into a pointer |
| Terminal showing sizeof 40 versus 8 | sizeof of array versus array parameter in embedded C programming |
| Bubble sort pass illustrated with pointers | Bubble sort using pointer arithmetic in a free C tutorial |
Summary and Key Takeaways
Remember
- An array parameter is a pointer. Brackets in a parameter list do not make an array.
sizeofinside the function gives pointer size. Pass the length separately.- You may reassign an array parameter, but only the local copy changes.
- You may never assign to a real array variable.
arr + klets a function start anywhere inside an array.
Conclusion
Array decay is the reason C code is full of (pointer, length) pairs. Once you stop expecting the array to travel with its size, buffer-handling code becomes straightforward and far safer. Next we step up a dimension and see what the same decay rules mean for two- and three-dimensional arrays.
FAQ: Array Parameters in C
Is an array passed by value or by reference in C?
Neither exactly. The array’s address is passed by value, so the callee can modify the caller’s elements but cannot see the array’s size.
Why does sizeof give 8 inside my function?
Because the parameter is a pointer, and pointers are 8 bytes on 64-bit systems. On a 32-bit microcontroller it would be 4.
Can I make the function receive a copy of the array?
Not directly. Wrap the array in a struct and pass the struct by value, or copy it yourself with memcpy.
What does passing arr + 3 do?
It passes the address of element 3, so the callee treats that element as its index 0.
Should I use a sentinel value or a length parameter?
Use a length parameter. Sentinels are only safe when the data format guarantees them, as with null-terminated strings.
Build Strong C Foundations for Firmware
Continue the free embedded C course with multidimensional arrays and the pointer types behind them.
Need Help or More Information?
Questions about this free C programming course, embedded C training or career guidance? Get in touch.

2 Comments