Pointers e Arrays
Quando um array é usado na maioria das expressões, ele 'decai' em um pointer para seu primeiro elemento — por isso int *p = numbers funciona sem precisar de &. Uma vez que p aponta para o início do array, p + 1 não significa 'some 1 ao endereço' — significa 'avance um int', com o compilador escalando automaticamente por sizeof(int).
#include <stdio.h>
int main() {
int numbers[3] = {10, 20, 30};
int *p = numbers;
printf("%d %d %d\n", *p, *(p + 1), *(p + 2));
printf("%d %d %d\n", p[0], p[1], p[2]);
return 0;
}10 20 30 10 20 30
numbers[i] é literalmente definido como *(numbers + i) — a indexação com colchetes é aritmética de pointers com uma sintaxe mais amigável, não uma característica de linguagem separada. É por isso que arrays em C não têm verificação de limites: numbers[i] é apenas um cálculo de endereço, e o compilador não tem como saber se esse endereço ainda está dentro do array.
A seguir: strings — por que C não tem um tipo string embutido, e o que um string realmente é por baixo dos panos.
Practice exercise
Dado int numbers[3] = {10, 20, 30} e int *p = numbers, imprima o segundo elemento usando aritmética de pointers (*(p + 1)).