Why this matters
In the arrays module you saw that an array passed to a function becomes "the address of its first element", and that gcc described a[i] as *(a + i * 4). This lesson makes that precise. In C, arrays and pointers are tied together by one rule: indexing is pointer arithmetic. Once you see it, a lot of C code that looked cryptic (while (*p) p++;, end - begin, *dst++ = *src++) becomes readable, and you understand exactly what the machine does when you write a[i].
Adding an integer to a pointer
If p points to an element of an array, p + 1 points to the next element, not the next byte. The compiler multiplies by the element size for you:
p + n holds the address (address in p) + n * sizeof(*p)
#include <stdio.h>
int main(void)
{
int a[4] = {10, 20, 30, 40};
int *p = a; /* same as &a[0] */
for (int i = 0; i < 4; i++) {
printf("p + %d = %p *(p + %d) = %d\n", i, (void *)(p + i), i, *(p + i));
}
printf("(p + 3) - p = %td elements\n", (p + 3) - p);
return 0;
}
One run printed:
p + 0 = 0x7fff16341ac0 *(p + 0) = 10
p + 1 = 0x7fff16341ac4 *(p + 1) = 20
p + 2 = 0x7fff16341ac8 *(p + 2) = 30
p + 3 = 0x7fff16341acc *(p + 3) = 40
(p + 3) - p = 3 elements
The addresses differ on each run, but they always go up by 4, the size of an int.
p p+1 p+2 p+3 p+4 (one past the end)
| | | | |
v v v v v
+--------+--------+--------+--------+
a | 10 | 20 | 30 | 40 |
+--------+--------+--------+--------+
ac0 ac4 ac8 acc ad0
Pointer arithmetic is only defined within one array (plus the position just past its end). Moving a pointer before the start or more than one past the end is undefined behavior even if you never dereference it.
a[i] is *(a + i)
The C standard defines the subscript operator this way: E1[E2] means *((E1) + (E2)). Two facts make that work with arrays:
- Array decay. In almost every expression, an array's name is converted to a pointer to its first element.
int *p = a;is the same asint *p = &a[0];. - Scaling.
a + ithen points at elementi, and*reads it.
So these are all the same element:
a[2] *(a + 2) p[2] *(p + 2) /* with p == a */
Indexing works on any pointer, relative to where it points:
#include <stdio.h>
int main(void)
{
int a[5] = {3, 1, 4, 1, 5};
int sum = 0;
for (const int *p = a; p < a + 5; p++) {
sum += *p;
}
printf("sum = %d\n", sum);
int *q = &a[2];
printf("q[0] = %d, q[1] = %d, q[-1] = %d\n", q[0], q[1], q[-1]);
printf("index of q = %td\n", q - a);
return 0;
}
sum = 14
q[0] = 4, q[1] = 1, q[-1] = 1
index of q = 2
q[-1] is fine here because q - 1 is still inside the array (it is a[1]).
The first loop is the classic pointer loop: start at the beginning, stop at one past the end, p++ moves to the next element. It does the same as an index loop. At -O0 it may save an instruction; with optimization on, compilers produce the same code for both. Use whichever reads better.
Subtracting pointers
Subtracting two pointers into the same array gives the number of elements between them (not bytes). The result has type ptrdiff_t (a signed integer type from <stddef.h>), printed with %td. That is how you turn a pointer back into an index: p - a.
Comparisons (<, <=, ==, !=) between pointers into the same array compare positions, which is what makes p < a + 5 a loop condition. Comparing pointers into different objects with < is undefined; == and != are always allowed (and comparing with NULL is how you check for "no object").
You cannot add two pointers (p + q is an error: invalid operands to binary +), and there is no multiplying them. Only pointer plus integer, pointer minus integer, and pointer minus pointer.
Ranges: begin and end
A pair of pointers, begin and end, where end points one past the last element, describes a range: the elements from *begin up to but not including *end. The length is end - begin, and an empty range has begin == end. That is why C explicitly allows forming (but not dereferencing) the one-past-the-end pointer: loops like for (p = begin; p != end; p++) need it. You will see this "half-open range" style throughout C and C++ code.
Where arrays and pointers differ
Array decay is so pervasive that people say "arrays are pointers". They are not. An array is a block of elements; a pointer is a variable holding one address. Decay does not happen in three places:
#include <stdio.h>
void show(int *a)
{
printf("in show: sizeof a = %zu\n", sizeof a);
}
int main(void)
{
int a[5] = {0};
int *p = a;
printf("sizeof a = %zu, sizeof p = %zu\n", sizeof a, sizeof p);
printf("a = %p, &a[0] = %p, &a = %p\n", (void *)a, (void *)&a[0], (void *)&a);
printf("a + 1 = %p, &a + 1 = %p\n", (void *)(a + 1), (void *)(&a + 1));
show(a);
return 0;
}
sizeof a = 20, sizeof p = 8
a = 0x7fff21252080, &a[0] = 0x7fff21252080, &a = 0x7fff21252080
a + 1 = 0x7fff21252084, &a + 1 = 0x7fff21252094
in show: sizeof a = 8
sizeof ais the whole array (20 bytes);sizeof pis one pointer (8).&ais the address of the whole array. Numerically it equals&a[0], but its type is "pointer to array of 5 ints", so&a + 1jumps 20 bytes (0x...80to0x...94), past the entire array.- A string literal initializing a char array (
char s[] = "hi";) copies the characters instead of decaying.
Also, an array name is not a variable you can change: a++ or a = p fail with lvalue required as increment operand and assignment to expression with array type. A pointer variable can be moved freely.
Inside show, the parameter a is a pointer, so sizeof gives 8. That is the full story behind the warning you met in the arrays module.
Common mistakes
*p + 1versus*(p + 1): the first is "the element plus one", the second "the next element". Unary*binds tighter than binary+.- Scaling twice:
p + i * sizeof(int)movesi * 4elements. The compiler already scales. - Running off the end:
p <= a + nvisits one element too many. Stop ata + n. - Subtracting pointers into different arrays: meaningless and undefined.
- Thinking
sizeofworks on decayed arrays: only on the array itself, never through a pointer.
Recap
p + nmovesnelements, i.e.n * sizeof *pbytes. Only valid within one array and one past its end.- An array name decays to a pointer to its first element;
a[i]is defined as*(a + i), and works on any pointer. q - pis the element distance (ptrdiff_t,%td); pointers into one array can be compared.begin/endpairs describe half-open ranges;endmay point one past the last element but must not be dereferenced.- Arrays are not pointers:
sizeof,&a, and string-literal initialization do not decay, and array names cannot be assigned.
Going further: write a loop that prints a double array using a double * and check, by printing addresses with %p, that each step moves 8 bytes. Then try a char * over the same memory (cast with (char *)) and watch it move one byte at a time.