The subscript expression a[i] is defined as *(a + i) - given an address a1, offset i elements (not bytes! - more on that below) from that address and dereference the result. ary[0] is equivalent to the expression *(ary + 0), which is equivalent to *ary, ary[1] is equivalent to *(ary + 1), etc.
The expression p[-2] is equivalent to *(p - 2); since p == ary + 3, then p - 2 == ary + 3 - 2 == ary + 1, which is the address of the second element of ary. Thus, p[-2] == ary[1] == 2.
Pointer arithmetic is based on the size of the pointed-to type - if p contains the address of an object of type T, then p + 1 yields the address of the next object of that type. For example, given
char *cp = (char *) 0x1000;
short *sp = (short *) 0x1000;
long *lp = (long *) 0x1000;
then we have the following:
Address char short long
------- +---+ +---+ +---+
0x1000 | | cp | | sp | | lp
+---+ + - + + - +
0x1001 | | cp + 1 | | | |
+---+ +---+ + - +
0x1002 | | cp + 2 | | sp + 1 | |
+---+ + - + + - +
0x1003 | | cp + 3 | | | |
+---+ +---+ +---+
0x1004 | | cp + 4 | | sp + 2 | | lp + 1
+---+ + - + + - +
... ... ...
- Arrays are not pointers - however, unless it is the operand of the
sizeof or unary & operators, or is a string literal used to initialize a character array in a declaration, an expression of type "N-element array of T" will be converted, or "decay", to an expression of type "pointer to T" and the value of the expression will be the address of the first element of the array.
ary + 3has the same value as&ary[3].p[1]andp[2]are indexing beyond the last element of the original array, which is undefined behaviour and the values found are irrelevant.sizeof(p) = %luis undefined behavior and flat out wrong on 64-bit Windows systems wheresize_tis 64 bits andlongis 32 bits.