The Secret Identity of Arrays

Three chapters of IOUs come due at once. Why does scanf("%19s", name) take no &? Why do arrays pass to functions without copying? Why did %s walk memory so naturally? One rule answers all of it: in almost every expression, an array converts to a pointer to its first element. The rule is called decay, and with it, arrays and pointers snap together into one picture.

Decay, and What a[i] Really Is

int *p = values compiles with no & because values in that expression already is a pointer to values[0]: decay in action. Then the equivalence that explains indexing itself: a[i] is defined as *(a + i), take the start, move i elements, dereference. The bracket syntax you have used since chapter 7 was pointer arithmetic all along, which is why p[2] works on a plain pointer, and why (a famous exam curiosity) 2[values] is legal: addition commutes.

Pointer arithmetic scales by the element type. p + 1 does not add one byte; it adds one int, four bytes here, because p is an int *. The type declared in chapter 2, the contiguous layout from chapter 7, and the typed pointers from last lesson combine into: address arithmetic in units of elements.

Walking with a Pointer

The chapter 7 index walk has a pointer twin:

end points one past the last element, and the standard explicitly blesses forming that address (dereferencing it remains out of bounds, the chapter 7 rule in pointer clothes). The p < end walk is idiomatic C, the shape %s and strlen use inside, and comparing or subtracting pointers is defined exactly when both point into the same array. Index walks and pointer walks compile to the same machine code on any modern compiler; the course uses whichever reads clearer, and you must read both fluently, because exams print both.

Arrays in Function Calls: The Whole Truth

Now the chapter 9 and 10 mysteries resolve in one stroke. When you pass an array to a function, decay happens at the call: the function receives a pointer to the first element, never a copy. That is why array arguments take no &, why passing a million-int array is as cheap as passing one address, and why void f(int arr[]) and void f(int *arr) declare the same function, the [] in a parameter list being pure notation.

Two consequences, both load-bearing. First, the function can modify the caller's elements, because it holds their real addresses: your array-filling loops have been out-parameters all along. Second, the function cannot know the array's length: it holds one address, not the object, so sizeof(arr) inside gives the size of a pointer, the classic trap, and the length travels as a separate parameter:

The pointer-plus-count pair is C's array-passing convention, visible in every real API. And structs? They copy whole even with arrays inside (chapter 10's puzzle) because decay applies to array expressions, and a struct argument is a struct expression: its arrays are members of the copied value, not expressions decaying on their own. Both halves of the contradiction were telling the truth.

The Exceptions to Decay

The rule says almost every expression, and the exceptions are exactly two you have met: sizeof(values) measures the whole array (chapter 7's element-count idiom depends on it), and &values takes the address of the entire array object. Everywhere else, string literals initializing arrays aside, array names are pointers-in-waiting. The sizeof exception working only in the declaring scope, and turning into pointer-size inside functions, is the sharpest version of the trap, and exams sharpen it further.

Key Takeaways

  • Decay: an array expression becomes a pointer to its first element; a[i] is *(a + i), and indexing works on any pointer.
  • Pointer arithmetic moves in elements, not bytes; one-past-the-end may be formed and compared, never dereferenced.
  • The p < end pointer walk is idiomatic; read it and the index walk interchangeably.
  • Functions receive arrays as pointers: no copy, no &, caller elements modifiable, length passed separately; int arr[] and int *arr are the same parameter.
  • sizeof inside a function measures the pointer, not the array: the trap that makes the separate count parameter mandatory.
  • Exceptions to decay: sizeof and & on the array name, in the declaring scope.