Eu vou tentar fazer algo extremamente arriscado, que é explicar de forma breve como funcionam ponteiros e alocação de memória. Se por um acaso ficar confuso, faz mais perguntas que eu ou outras pessoas te respondemos.
Quando você aloca memória para um ponteiro, o esquema funciona mais ou menos da seguinte maneira:
int *v = malloc(5 * sizeof(int));
O malloc
vai tentar reservar uma sequência de memória de tamanho 5 * tamanho de um int
. Se o int tem 4 bytes, essa chamada em particular vai tentar reservar 5 * 4
bytes, que no final são 20
bytes. O retorno dessa chamada é o primeiro endereço dessa lista de bytes alocados.
Imagine que isso aqui é a memória, e cada posição representa 1 byte e tem um endereço:
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
Cada inteiro precisa de 4 posições dessas para ser representado. Portanto, o primeiro número é representado com os 4 primeiros endereços. O segundo com os 4 seguintes, e assim por diante.
Quando você faz isso:
int primeiro = v[0];
Você está dizendo: eu quero que você leia os 4 primeiros bytes a partir desse endereço v
, interprete como int
e atribua na variável primeiro
.
Em seguida, você pode fazer:
int segundo = v[1];
Que quer dizer: eu quero que você leia os 4 primeiros bytes a partir do endereço v + (1 * tamanho de int)
, interprete com int
e atribua na variável segundo
.
Esse número que vai dentro do operador []
significa quantas vezes o tamanho de um int
(no caso de um ponteiro para inteiros) deve ser pulado a partir do primeiro endereço da sequência de memória. Pesquisa sobre aritmética de ponteiros se quiser saber mais sobre isso.
A aritmética de endereços e feita exatamente da mesma forma para todos os tipos de dados. Acontece que ponteiros também são um tipo de dado.
Por isso, quando você declara e inicializa uma variável como:
int **ponteiroParaPonteiros = malloc(5 * sizeof(int*));
Você está alocando 5 * o tamanho de um ponteiro para int
. Cada posição desse array de bytes alocado pode ser interpretado como um ponteiro para inteiro. Por isso você pode fazer assim:
int *primeiroPonteiro = ponteiroParaPonteiros[0];
Esse negócio é extremamente cabeludo a primeira vista, mas uma hora tudo vai clicar e você vai compreender. Uma forma que me ajuda a compreender o tipo de dado quando tem um monte de símbolo de ponteiros, é ler a definição da direita para a esquerda.
Por exemplo:
int * a;
Lendo da direita para a esquerda: a
é um ponteiro para inteiros
int ** a;
a
é um ponteiro para ponteiros para inteiros.
E assim sucessivamente.