Pointer Arithmetic and Array Decay
An array name decays to a pointer to its first element and ptr + i steps by whole elements, so you can walk arrays with pointers and pass them to functions alongside an explicit length.
One Step Means One Element
Chapter 3 left you a sentence worth cashing in: a pointer carries a type, and the type is what makes the dereference mean anything. That type has a second job, and this whole lesson rests on it. You may add an integer to a pointer, and the result is not the address that many bytes along. It is the address that many elements along.
#include <stdio.h>
int main(void)
{
int scores[5] = {90, 72, 85, 61, 78};
double rates[3] = {1.5, 2.5, 3.5};
int *p = &scores[0];
double *q = &rates[0];
printf("p is %p\n", (void *)p);
printf("p + 1 is %p\n", (void *)(p + 1));
printf("p + 3 is %p\n", (void *)(p + 3));
printf("q is %p and q + 1 is %p\n", (void *)q, (void *)(q + 1));
return 0;
}
p is 0xfbffa7af0020
p + 1 is 0xfbffa7af0024
p + 3 is 0xfbffa7af002c
q is 0xfbffa7af0060 and q + 1 is 0xfbffa7af0068
Read the differences and not the digits, for the reason lesson 1 gave: address space layout randomization moves everything on every run, so your numbers will differ from these and from your own previous run. p + 1 is 4 past p, which is one int. p + 3 is 12 past, which is three of them, since 0x20 plus 12 is 0x2c. And q + 1, the identical expression written on a double *, is 8 past q. Nothing in the source said 4 or 8. The rule is that p + i is i * sizeof(*p) bytes past p, the compiler taking the scale factor from the pointer's type, which is why the arithmetic lands on element boundaries whatever the elements are. It is also why the arithmetic only means anything inside an array: p + 1 names a real object precisely because lesson 1 guaranteed the elements sit back to back with no gaps between them.
Indexing Is Pointer Arithmetic
If p aims at scores[0] then p + 3 aims at scores[3], so *(p + 3) reads the object that scores[3] reads. Those are not two routes to one answer. They are one operation with two spellings, and the language defines the bracket form in terms of the other: a[i] means exactly *(a + i). Which raises a question about what a on its own can possibly be, given that + there wants a pointer.
#include <stdio.h>
int main(void)
{
int scores[5] = {90, 72, 85, 61, 78};
printf("scores is %p and &scores[0] is %p\n", (void *)scores, (void *)&scores[0]);
printf("scores[3] is %d and *(scores + 3) is %d\n", scores[3], *(scores + 3));
printf("sizeof(scores) is %zu but sizeof(scores + 0) is %zu\n", sizeof(scores), sizeof(scores + 0));
return 0;
}
scores is 0xfbffad7f0020 and &scores[0] is 0xfbffad7f0020
scores[3] is 61 and *(scores + 3) is 61
sizeof(scores) is 20 but sizeof(scores + 0) is 8
The first line answers it. In almost every expression an array name decays to a pointer to its first element, so scores used by itself is &scores[0] with type int *, and scores + 3 is then plain pointer arithmetic, which is why the second line comes out twice identical. "Almost every" has exactly three exceptions, and they are worth learning alongside the rule because you meet all three early. sizeof arr does not decay: the third line asks for the size of the array and gets all 20 bytes, then adds a harmless + 0 so that the same name decays first, and the answer drops to 8, the size of a pointer on this machine. That exception is the entire reason lesson 1's sizeof(scores) / sizeof(scores[0]) idiom worked. &arr does not decay either; it produces the address of the whole array rather than of its first element. The third exception is a string literal initializing an array, which belongs to chapter 5 and is named here only so the set is complete.
What Actually Arrives at a Function
Decay is not a curiosity. It is what happens at every call that passes an array. The array does not travel, a pointer to its first element does, so a function that takes an array is a function that takes a pointer. And since lesson 1 established that an array carries no length at run time, the length has to travel beside it as a parameter of its own.
#include <stdio.h>
void print_values(const int *values, int count)
{
for (int i = 0; i < count; ++i)
{
printf("[%d]", values[i]);
}
printf("\n");
}
int main(void)
{
int scores[5] = {90, 72, 85, 61, 78};
print_values(scores, 5);
print_values(scores, 3);
return 0;
}
[90][72][85][61][78]
[90][72][85]
void print_values(const int *values, int count) is the shape every array function in the rest of this course uses, and each piece of it is deliberate. count is a parameter because there is no other way for the function to find out. const is chapter 3's rule applied to arrays: this function reads and never writes, it says so in the signature, and a slip of the finger becomes a build error rather than a surprise in the caller. The two calls show the count doing real work, since the same array and the same pointer value yield two different amounts of reading. Notice as well that values[i] is written inside the function even though values is a pointer, which is not a special permission granted to parameters; values[i] was always *(values + i). You will also meet that parameter written int values[], or with a number in it as int values[10], and here is the fact to hold on to: all three spellings declare the same function, and a program that uses all three behaves identically. A parameter cannot have array type in C, so the compiler rewrites it to a pointer, and a bound written there is documentation for whoever reads the signature. It is not part of the type, nothing at run time enforces it, and the function still has no way to ask how many elements it was handed. (gcc at -O2 will sometimes take a written bound as a hint and warn when it can see the call and the array together, but that is a compiler being helpful about one visible call site, not the language checking anything.)
The Number That Lies
Now the consequence, and it is the array mistake that catches nearly every C programmer exactly once. The next program is wrong on purpose.
#include <stdio.h>
void report(const int *values)
{
printf("the function says %zu\n", sizeof(values) / sizeof(values[0]));
}
int main(void)
{
int scores[5] = {90, 72, 85, 61, 78};
printf("main says %zu\n", sizeof(scores) / sizeof(scores[0]));
report(scores);
return 0;
}
trap.c:5:54: warning: division 'sizeof (const int *) / sizeof (int)' does not compute the number of array elements [-Wsizeof-pointer-div]
main says 5
the function says 2
One array, one idiom, two different answers. In main, scores is a real array and sizeof is a decay exception, so the idiom reports 5 exactly as lesson 1 promised. Inside report the parameter is a pointer, so sizeof(values) is 8 and sizeof(values[0]) is 4, and the division reports 2. It does not fail, it lies, handing back a plausible number, and a loop bounded by that number reads two elements out of five here or, on some other combination of types, runs past the end into undefined behaviour. Spelling the parameter const int values[] changes nothing, since it is the same pointer parameter; gcc simply switches to a differently worded warning that says so out loud, that sizeof there "will return size of const int *". Treat those warnings as luck rather than as protection, because they recognise a shape rather than an idea. Assign the two sizeof results to variables and divide those instead, and this platform's compiler says nothing at all while still computing 2. The rule that does not depend on the compiler noticing is to pass the length as a separate parameter, every time.
Walking With a Pointer
Since values[i] is *(values + i), a loop can carry the moving pointer itself rather than move an index and add it each time.
#include <stdio.h>
void print_values(const int *values, int count)
{
for (const int *p = values; p < values + count; ++p)
{
printf("[%d]", *p);
}
printf("\n");
}
int main(void)
{
int scores[5] = {90, 72, 85, 61, 78};
print_values(scores, 5);
return 0;
}
That prints [90][72][85][61][78], the same line as the indexed version, because it visits the same objects: p starts at the first element and ++p steps one element on, by the scaling rule from the first section. The bound is the part to stare at. values + count is a pointer to one past the last element, and it is no accident that you are allowed to compute it, because C explicitly permits forming the address one past the end of an array, precisely so that loops can use it as a limit. What C does not permit is reading through it. *(values + count) is out of bounds by exactly lesson 1's rule, so it is undefined behaviour, and here AddressSanitizer aborts on it with a stack-buffer-overflow reporting a READ of size 4. The loop above never does that, because p < values + count is already false the moment p reaches that address. Form it, compare against it, never dereference it. Both loops are correct and you need to read both fluently, because real C code is full of the pointer form. This course's default is the indexed loop, because values[i] names the element out loud and the index is right there when you want to print or compare it; reach for the pointer walk when the moving pointer is genuinely the thing you mean. One shape will not change from here on: the rest of this chapter hands you arrays whose size is decided while the program runs, and every one of them is carried around as this same pair, a pointer to the first element and a count beside it.
Key Takeaways
p + imovesielements, notibytes, landingi * sizeof(*p)bytes along. The pointer's type supplies the scale, so the same+ 1steps 4 bytes on anint *and 8 on adouble *.a[i]is defined as*(a + i). Indexing is pointer arithmetic in friendlier spelling, which is why a pointer can be indexed and an array name can be added to.- An array name decays to a pointer to its first element in almost every expression. The three exceptions are
sizeof arr,&arr, and a string literal initializing an array (chapter 5). - Passing an array passes a pointer, so
void f(int *arr),void f(int arr[])andvoid f(int arr[10])all declare the same function and the bound is documentation only. Always pass the length as a separate parameter, and take the array asconst int *when the function only reads it. - Inside such a function
sizeof(arr) / sizeof(arr[0])is wrong and silent about it, dividing the size of a pointer by the size of an element and reporting 2 for an array of 5. gcc may warn with-Wsizeof-pointer-div, but rearranging the expression hides the warning, so never rely on the diagnostic. - Forming
arr + countis legal, dereferencing it is undefined behaviour. That is what makesfor (const int *p = arr; p < arr + count; ++p)a correct loop, and AddressSanitizer reports the dereference as astack-buffer-overflow.
Create an account to track your progress and access interactive exercises. Already have one? Sign in.
Pointer Arithmetic and Array Decay - Quiz
Test your understanding of the lesson.
Practice Exercises
Two Functions That Take an Array and a Length
Lesson 1 kept everything in main because an array handed to a function had not been explained yet. Now it has, so this exercise moves the work out into two functions with the signature the rest of the course uses: int sum_values(const int *values, int count) returns the total of the elements, and int max_value(const int *values, int count) returns the largest of them. main is written for you and does not need changing. It fills an array of capacity 5 with the loop from lesson 1, keeps count beside it, prints no numbers read and returns when nothing converted, and otherwise calls your two functions and prints their results. Both functions arrive with a body that does a fraction of the job, and replacing that fraction with a loop over all count elements is the exercise. Read the signature before you start, because everything you need to know is in it. values is a pointer, not an array, and that is not a compromise the exercise made; it is what a function receives when an array is passed, since the array name decays to a pointer to its first element at the call. So do not reach for sizeof inside these functions. sizeof(values) is the size of a pointer, the familiar sizeof(values) / sizeof(values[0]) idiom computes 2 here no matter how many numbers were read, and a loop bounded by that answer would sum two elements and call it the total. count is the parameter that carries the length, and it is the only bound either loop may use. const is the other half of the signature and it is a promise: both functions read and neither writes, so if you accidentally assign to values[i] or to *p the compiler stops you with an error about a read-only location rather than letting the bug reach a caller. You may write either loop shape. The indexed form for (int i = 0; i < count; ++i) with values[i] is this course's default, and the pointer form for (const int *p = values; p < values + count; ++p) with *p visits exactly the same objects because values[i] is defined as *(values + i); writing one of each is a good way to prove to yourself that they are the same loop. max_value has one care that sum_values does not, the same one lesson 1's largest loop had: start largest at values[0] rather than at 0, or an input of nothing but negative numbers reports a largest that never appeared in it. Reading values[0] is safe here only because main returns before calling you when count is 0. Every path returns 0, since the checker treats a nonzero exit status as a failure however correct the printed output looks.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!