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: