Não é o compilador que divide. É o tipo do ponteiro que determina como vão ficar os dados.
Por exemplo (vamos assumir que sizeof(int)
é 4), se você faz isso (e note pelo exemplo abaixo que não precisa fazer o cast do malloc
):
int *v = malloc(10 * sizeof(int));
É alocado um espaço de 40 bytes, e o ponteiro v
aponta para o início deste bloco. Ou seja, v
contém o endereço de memória no qual esse bloco se inicia. Mas nem o malloc
e nem o compilador sabem o que tem nesses bytes.
O “pulo do gato” é que se você fizer algo como v[i]
, na verdade estará acessando o valor do endereço v + i
- ou seja, v[i]
é equivalente a *(v + i)
.
Por exemplo, v[2]
é o mesmo que *(v + 2)
. Vamos por partes:
-
v + 2
pega o endereço contido em v
e soma 2 * sizeof(int)
- isso porque o tipo de v
é int *
(um ponteiro para int
). Ou seja, o tipo do ponteiro determina o deslocamento necessário. Na prática, v + 2
seria algo como “dois int
’s à frente de v
”. Vale notar que por causa desta regra, v[0]
equivale a *v
- o operador
*
pega o valor que está no endereço. Então *(v + 2)
pega o valor que está no endereço correspondente a v + 2
Por isso que ao alocar memória para um ponteiro, podemos usá-lo “como se fosse um array”. v[2]
é como se pegasse o elemento na posição 2 do “array”, e graças à aritmética de ponteiros (que usa o sizeof
do tipo do ponteiro para saber quantos bytes devem ser “pulados”), ele pega o elemento correto.
De novo: não é o malloc
e nem o compilador que “organiza” a memória. É o tipo do ponteiro que determina como ela é acessada e como os dados são guardados nesses bytes.
Só para complicar um pouco e mostrar como é o tipo que define, vamos supor que eu crie um inteiro e um ponteiro para ele (código compilado em gcc 8.3):
int x = 4096;
int *p = &x;
printf("sizeof int=%ld\n", sizeof(int)); // 4
printf("sizeof char=%ld\n", sizeof(char)); // 1 - na verdade não precisava porque sizeof(char) sempre é 1, mas enfim...
Ou seja, p
é um ponteiro que contém o endereço de x
. Vamos imprimir o endereço contido em p
e depois ver qual é o endereço resultante de p + 1
:
printf("p =%p\n", p);
// soma 1 ao ponteiro, novo endereço é p + sizeof(int)
printf("p + 1=%p\n", p + 1);
Claro que a saída vai variar a cada vez que vc executar, mas o importante é ver que p + 1
resulta em um endereço que está não 1 byte na frente de p
, e sim 4 bytes (pois sizeof(int)
é 4):
p =0x7ffef4413fc4
p + 1=0x7ffef4413fc8
Agora, se pegarmos o mesmo endereço e colocarmos em um ponteiro de outro tipo:
// "mudando o tipo"
char *c = (char*) p;
// c aponta para o mesmo endereço de p
printf("c =%p\n", c);
// mas agora somar 1 adiciona sizeof char
printf("c + 1=%p\n", c + 1);
A saída é:
c =0x7ffef4413fc4
c + 1=0x7ffef4413fc5
Repare que o endereço contido em c
é o mesmo de p
. Mas ao somar 1, vemos que foi adicionado 1 byte apenas (pois o tipo do ponteiro c
é char *
, e como sizeof(char)
é 1, então c + 1
soma 1 byte).
Por fim, também dá diferença ao obter o valor:
printf("%d\n%d", *p, *c);
Isso imprime:
4096
0
Pois o operador *
pega o valor que está no endereço para o qual o ponteiro aponta, então ele vai no endereço apontado e pega apenas os bytes necessários (e a quantidade é determinada pelo sizeof
do tipo do ponteiro).
O programa completo está aqui.
Por fim, vale lembrar que arrays e ponteiros não são a mesma coisa. O que acontece é que eles são, de certa forma, intercambiáveis. Para mais informações, não deixe de ler: