In pointer arithmetic, the integer to be added or subtracted to pointer is interpreted not as change of address but as number of elements to move.

#include <stdio.h>

int main(void) {
    int array[] = {1, 2, 3, 4, 5};
    int *ptr = &array[0];
    int *ptr2 = ptr + sizeof(int) * 2; /* wrong */
    printf("%d %d\\n", *ptr, *ptr2);
    return 0;
}

This code does extra scaling in calculating pointer assigned to ptr2. If sizeof(int) is 4, which is typical in modern 32-bit environments, the expression stands for “8 elements after array[0]”, which is out-of-range, and it invokes undefined behavior.

To have ptr2 point at what is 2 elements after array[0], you should simply add 2.

#include <stdio.h>

int main(void) {
    int array[] = {1, 2, 3, 4, 5};
    int *ptr = &array[0];
    int *ptr2 = ptr + 2;
    printf("%d %d\\n", *ptr, *ptr2); /* "1 3" will be printed */
    return 0;
}

Explicit pointer arithmetic using additive operators may be confusing, so using array subscripting may be better.

#include <stdio.h>

int main(void) {
    int array[] = {1, 2, 3, 4, 5};
    int *ptr = &array[0];
    int *ptr2 = &ptr[2];
    printf("%d %d\\n", *ptr, *ptr2); /* "1 3" will be printed */
    return 0;
}

E1[E2] is identical to (*((E1)+(E2))) (N1570 6.5.2.1, paragraph 2), and &(E1[E2]) is equivalent to ((E1)+(E2)) (N1570 6.5.3.2, footnote 102).

Alternatively, if pointer arithmetic is preferred, casting the pointer to address a different data type can allow byte addressing. Be careful though: endianness can become an issue, and casting to types other than ‘pointer to character’ leads to strict aliasing problems.

#include <stdio.h>

int main(void) {
    int array[3] = {1,2,3};  // 4 bytes * 3 allocated
    unsigned char *ptr = (unsigned char *) array;  // unsigned chars only take 1 byte
    /*
     * Now any pointer arithmetic on ptr will match
     * bytes in memory.  ptr can be treated like it
     * was declared as: unsigned char ptr[12];
     */

    return 0;
}