An lvalue is an expression of object type other than void
that potentially designates an object (a chunk of memory that can potentially store values), such that the object may be read or modified. Lvalues may include variable names like x
, array subscript expressions like a[i]
, member selection expressions like foo.bar
, pointer dereferences like *p
, etc. A good rule of thumb is that if it can be the target of the =
operator, then it's an lvalue.
Arrays are weird. An array expression is an lvalue, but it's a non-modifiable lvalue; it designates an object, but it cannot be the target of an assignment. When you declare an array in C like
int a[N];
what you get in memory looks something like this:
+---+
a: | | a[0]
+---+
| | a[1]
+---+
| | a[2]
+---+
...
There's no object a
that's separate from the individual array elements; there's nothing to assign to that's named a
. a
represents the whole array, but C doesn't define the =
operator to work on a whole array.
Brief history lesson - C was derived from an earlier language named B, and when you declared an array in B:
auto a[N];
you got something like this:
+---+
a: | | -------------+
+---+ |
... |
+---+ |
| | a[0] <-------+
+---+
| | a[1]
+---+
| | a[2]
+---+
...
In B, a
was a separate object that stored an offset to the first element of the array. The array subscript operation a[i]
was defined as *(a + i)
- given a starting address stored in a
, offset i
words1 from that address and dereference the result.
When he was designing C Ritchie wanted to keep B's array behavior (a[i] == *(a + i)
), but he didn't want to keep the explicit pointer that behavior required. Instead, he created a rule that any time an array expression isn't the operand of the sizeof
, _Alignof
, or unary &
operators, it is converted, or "decays", from type "N-element array of T
" to "pointer to T
" and the value of the expression is the address of the first element.
The expression a[i] = *(a + i)
works the same as it did in B, but instead of storing the address of the first element in a
, we compute that address as we need it (this is done during translation, not runtime). But it means you can use the []
subscript operator with pointers as well, so ptr[i]
does the same thing:
+---+ +---+
a: | | a[0] (ptr[0]) <------ ptr: | |
+---+ +---+
| | a[1] (ptr[1])
+---+
| | a[2] (ptr[2])
+---+
...
And this is why a
cannot be the target of an assignment - under most circumstances, it "decays" to a pointer value equivalent to &a[0]
, and values cannot be the target of an assignment.
You cannot change the address of something - you can only change the value stored at a given address.
- B was a typeless language - everything was stored as a word.