Array expressions are special in C.
6.3.2.1 Lvalues, arrays, and function designators
...
3 Except when it is the operand of the sizeof
operator, the _Alignof
operator, or the
unary &
operator, or is a string literal used to initialize an array, an expression that has
type ‘‘array of type’’ is converted to an expression with type ‘‘pointer to type’’ that points
to the initial element of the array object and is not an lvalue. If the array object has
register storage class, the behavior is undefined.
C 2011 online draft
When you call
set_array(b);
the expression b
is converted from type "4-element array of int
" to "pointer to int
" (int *
), and the value of the expression is the address of b[0]
. Therefore, what set_array
receives is a pointer value (&b[0]
), not an array.
6.7.6.3 Function declarators (including prototypes)
...
7 A declaration of a parameter as ‘‘array of type’’ shall be adjusted to ‘‘qualified pointer to
type’’, where the type qualifiers (if any) are those specified within the [
and ]
of the
array type derivation. If the keyword static
also appears within the [
and ]
of the
array type derivation, then for each call to the function, the value of the corresponding
actual argument shall provide access to the first element of an array with at least as many
elements as specified by the size expression.
ibid.
Basically, in a function definition, any parameter declared as T a[N]
or T a[]
shall be interpreted as T *a
- IOW, the parameter is treated as a pointer, not an array. Your function definition
void set_array(int array[4]){
array[0] = 22;
}
is treated as though it was written
void set_array(int *array){
array[0] = 22;
}
So arrays kind-of-sort-of-but-not-really get passed by reference in C. What really happens is that a pointer to the first element of the array gets passed by value. The array
parameter in set_array
designates a distinct object in memory from b
, so any changes you make to array
itself have no effect on b
(IOW, you could assign a new value to array
and have it point to a different object, but this assignment doesn't affect b
at all). Instead, what you are doing is accessing elements of b
through array
.
In picture form:
+---+
b: | | b[0] <---+
+---+ |
| | b[1] |
+---+ |
| | b[2] |
+---+ |
| | b[3] |
+---+ |
... |
+---+ |
array: | | ---------+
+---+
One practical consequence of this is that sizeof array
yields the size of the pointer type int *
, not the size of the array, unlike sizeof b
. That means you can't count the number of elements in the array using the sizeof array / sizeof array[0]
trick. When you pass an array as an argument to a function, you should also pass the size of the array (that is, the number of elements) as a separate parameter unless the array contains a well-defined sentinel value (like the 0 terminator in strings).
Remember that the expression a[i]
is defined as *(a + i)
- given a starting address a
, offset i
elements (not bytes!) from that address and deference the result. If a
is an array expression, it is first converted to a pointer expression before this computation is done. That's why you can access elements of b
through array
- array
simply stores the address of the first element of b
.
In C, all function arguments are passed by value - the formal argument in the function definition and the actual argument in the function call refer to distinct objects in memory, and the value of the actual argument gets copied to the formal argument. Changes to the formal argument are not reflected in the actual argument.
We fake pass-by-reference semantics by passing a pointer to the actual parameter and writing to the dereferenced pointer:
void foo( T *ptr )
{
*ptr = new_value(); // write a new value to the thing ptr points to
}
void bar( void )
{
T var;
foo( &var ); // have foo write a new value to var
}
IOW, writing to *ptr
is the same as writing to var
. Writing to ptr
, OTOH, has no affect outside of foo
.
In a true pass-by-reference system (like, say, Fortran) both the formal argument in the function definition and the actual argument in the function call designate the same object (in the "thing that takes up memory and can store values" sense, not the object-oriented "instance of a class" sense), so in those systems any changes to the formal argument are reflected in the actual argument (leading to a classic question on the Hacker Test, "Have you ever changed the value of 4
? Unintentionally? In a language other than Fortran?")