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 + i moves i elements, not i bytes, landing i * sizeof(*p) bytes along. The pointer's type supplies the scale, so the same + 1 steps 4 bytes on an int * and 8 on a double *.
  • 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[]) and void 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 as const 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 + count is legal, dereferencing it is undefined behaviour. That is what makes for (const int *p = arr; p < arr + count; ++p) a correct loop, and AddressSanitizer reports the dereference as a stack-buffer-overflow.