C / POINTERS
Arrays decaying to pointers in function calls
Explain why an array argument arrives inside a function as a plain pointer, predict what sizeof reports on each side, and keep lengths available.
What you will learn
- Name the three contexts where an array does not decay: sizeof, _Alignof, unary &
- Read int a[10] in a parameter list as int *a and predict sizeof on both sides
- Preserve a length by passing a count, or by taking int (*a)[N] and calling with &arr
- Explain why int g[2][3] fits int (*)[3] but never int **
Understanding Arrays decaying to pointers in function calls
C has no mechanism for passing an array by value: there is no array assignment, and an argument slot cannot carry 40 bytes of int. So wherever an array expression appears, the compiler converts it to a pointer to element zero, changing its type from int[10] to int *. That conversion is called decay, and it happens everywhere except in three positions: as the operand of sizeof, as the operand of _Alignof, and as the operand of unary &.
The parameter side has its own rule that meets decay halfway. A parameter written int block[10] is adjusted by the compiler to int *block, and the 10 is discarded without ever being checked against the argument. That is why sizeof on that parameter reports the size of a pointer, and why an assignment to block[2] changes the caller's array: you were handed the original object's address, never a copy. The length is knowledge attached to the array's declaration, and declarations do not cross a function call.
The practical consequence is that a function holding a pointer must be told how many elements exist, which is why so many C library functions take a size argument beside the pointer. If the length is fixed at compile time you can instead declare the parameter as int (*a)[10] and call it with &arr, since & is one of the positions where no decay occurs, so the 10 survives inside the type. For a two-dimensional array only the outermost dimension is stripped: int g[2][3] decays to int (*)[3], which keeps the row length that indexing g[r][c] needs, and that is exactly why int ** is a different, wrong type.
<stdio.h>
/* The 10 below is documentation only: the compiler rewrites this
parameter as "int *block". */
void inspect(int block[10])
{
printf("inside: sizeof(block) = %zu, block[2] = %d\n",
sizeof block, block[2]);
block[2] = 99; /* writes into main's array, not a copy */
}
int main(void)
{
int data[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
printf("outside: sizeof(data) = %zu, data[2] = %d\n",
sizeof data, data[2]);
inspect(data); /* data decays to &data[0] here */
printf("after: data[2] = %d\n", data[2]);
return 0;
}
Passing an array to a function passes only the address of its first element, so the array's length lives in the caller's declaration and nowhere in the call.
Worked examples
Decayed pointer versus pointer to the whole array
Shows that a and &a hold the same address but have different types and therefore different step sizes.
<stdio.h>
int main(void)
{
int a[5] = {10, 20, 30, 40, 50};
int *first = a; /* decay: a becomes &a[0], type int * */
int (*all)[5] = &a; /* no decay: type int (*)[5], keeps the 5 */
printf("sizeof a = %zu\n", sizeof a);
printf("sizeof first = %zu\n", sizeof first);
printf("sizeof *first = %zu\n", sizeof *first);
printf("sizeof *all = %zu\n", sizeof *all);
printf("same address? %s\n", (void *)first == (void *)all ? "yes" : "no");
printf("(*all)[3] = %d\n", (*all)[3]);
return 0;
}
Example explained
Line 1int *first = a; uses the decayed value, so first points at a[0] and knows nothing about the other four elements.
Line 2int (*all)[5] = &a; the & operator suppresses decay, so the element count 5 stays inside the type.
Line 3sizeof *first is 4 (one int) while sizeof *all is 20 (the entire array), so all + 1 would jump past a completely.
Line 4The equality test prints yes: the two pointers hold the same address, and only their types, and hence their arithmetic, differ.
A 2D array decays to a pointer to its first row
Demonstrates that only the outer dimension is lost, so the row length must appear in the parameter type.
<stdio.h>
/* grid is really int (*grid)[3]; the row length 3 is required,
the number of rows is not part of the type. */
int sum_rows(int grid[][3], size_t rows)
{
int total = 0;
for (size_t r = 0; r < rows; r++)
for (size_t c = 0; c < 3; c++)
total += grid[r][c];
return total;
}
int main(void)
{
int grid[2][3] = {{1, 2, 3}, {4, 5, 6}};
printf("sizeof grid = %zu\n", sizeof grid);
printf("sizeof grid[0] = %zu\n", sizeof grid[0]);
printf("total = %d\n", sum_rows(grid, 2));
return 0;
}
Example explained
Line 1sizeof grid is 24 because grid is the operand of sizeof and does not decay: 2 rows * 3 ints * 4 bytes.
Line 2grid[0] is itself an array of 3 int, so sizeof grid[0] is 12, which is the row stride the compiler uses.
Line 3In sum_rows the parameter int grid[][3] is adjusted to int (*grid)[3], so grid[r][c] can be computed as base + r*12 + c*4.
Line 4int ** would fail because grid stores 6 contiguous ints and no row pointers anywhere for such a type to follow.
Blocking decay with a pointer to array
Passes &arr so the callee can recover the element count from the type instead of an extra argument.
<stdio.h>
void fill(int (*a)[4])
{
size_t n = sizeof *a / sizeof (*a)[0]; /* works: type still has the 4 */
for (size_t i = 0; i < n; i++)
(*a)[i] = (int)(i * i);
printf("callee knows n = %zu\n", n);
}
int main(void)
{
int nums[4];
fill(&nums); /* &nums has type int (*)[4], no decay */
for (size_t i = 0; i < 4; i++)
printf("nums[%zu] = %d\n", i, nums[i]);
return 0;
}
Example explained
Line 1fill takes int (*a)[4] and &nums produces exactly that type, so no array-to-pointer conversion happens at the call.
Line 2sizeof *a is 16, the size of the whole pointed-to array, so the division yields the real count 4 inside the callee.
Line 3Elements are written as (*a)[i] because a points at an array, not at an int; a[0][i] is the same thing.
Line 4The price of this technique is rigidity: fill accepts arrays of exactly 4 int and nothing else.
Important notes
The byte counts here assume 4-byte int and 8-byte pointers; the point is that an array's size and a pointer's size are unrelated, not the specific numbers.
int a[], int a[10] and int *a are the same parameter type; C99's int a[static 10] lets the compiler assume at least 10 elements exist, but sizeof a is still the pointer size.
Common mistakes
Writing sizeof p / sizeof p[0] inside a function whose parameter is int p[]: on a 64-bit machine that is 8/4 = 2, so the loop touches two elements and silently ignores the rest of the array.
Passing int g[2][3] to a parameter of type int **, often after casting the warning away: the callee reinterprets the first ints as addresses and dereferences them, which normally crashes.
Returning the name of a local array from a function: it decays to a pointer, compiles cleanly, and the caller then reads storage whose lifetime has ended, so the values change with the next call.
Try it yourself
Change, predict, then run
Write int sum(const int *v, size_t n) plus a broken twin int sum_bad(const int v[8]) that derives its own count with sizeof v / sizeof v[0]. Call both on the same 8-element array and print the two totals to see how many elements each one actually visited.
Open the C workspaceCheck your understanding
void f(double v[100]); is called as f(temps) where temps is double temps[100]. Inside f, sizeof v / sizeof v[0] evaluates to 1 on a machine with 8-byte doubles and 8-byte pointers. Why?
- The parameter type is adjusted to double *, so sizeof v is the size of a pointer (8) and sizeof v[0] is 8
- The array was copied into f, but only the first element fitted in the parameter, so the copy has length 1
- sizeof cannot be applied to a parameter, so the compiler substitutes 1 and warns
- The 100 is checked only at the call site and is then reset to 1 for the body of f
Show answer
A parameter declared as an array is rewritten as a pointer, so v is a double * and sizeof v gives 8 while sizeof v[0] gives 8, making the quotient 1. The copy answer is tempting but wrong in a deeper way: nothing is copied at all, the call transmits only the address of temps[0], which is exactly why writes through v are visible to the caller.