Dúvida sobre a função malloc (C)

Olá, estou com dúvida no funcionamento de malloc em C (função da biblioteca padrão usada para alocar memória depois que o programa iniciou a execução).

A formatação normalmente escrita para a alocação dinâmica de um vetor de 10 inteiros pode ser como:

int *v;
v = (int *) malloc(10*sizeof(int));

Minha dúvida é como o compilador saberia que estou querendo 10 espaços de memória com 4 bytes cada? Sim, eu digo isso nos parâmetros da função, mas o que eu escrevi é 40 bytes, pois o int se aplica ao operador sizeof, o malloc não está reconhecendo int. Por que o computador não poderia interpretar e reservar 4 espaços de 10 bytes ao invés de 10 espaços com 4 bytes?
Obrigado.

Primeiramente, sizeof(qualquer coisa) não tem tamanho fixo, depende do compilador. Se hoje em dia em muitas implementações sizeof(int) é 4, é um mero detalhe.

Quanto ao malloc, tudo que ele faz é alocar uma determinada quantidade de bytes, e só.

Você sabe que serão 10 inteiros porque vc declarou um ponteiro para int, e sendo assim o tamanho total é 40. Mas nada impede que outros tipos resultem no mesmo tamanho. Por exemplo, o código abaixo (usando gcc 8.3) imprime 40 duas vezes:

#include <stdio.h>
 
struct dados {
  int a, b;
  char *s;
  char *v;
  char *x;
  char *y;
};
 
int main(void) {
    printf("%d\n", 10 * sizeof(int));
    printf("%d\n", sizeof(struct dados));
    return 0;
}

Veja aqui esse código rodando.

Se vc alocar 40 bytes, não é o malloc que sabe o que tem neles, é o tipo que vc usou que vai determinar o que será gravado nesses bytes.

Sim. A minha dúvida era se 10*sizeof(int) = 40 bytes, como o computador entenderia que eu quero uma área de memória contínua de 10 espaços de 4 bytes, como o computador saberia que se trata de um vetor com 10 elementos inteiros. De acordo com o que eu li o malloc retorna um endereço de um bloco de memória com o tamanho especificado no parâmetro do malloc em bytes. Você então respondeu a minha pergunta, o tipo que o ponteiro aponta irá determinar como esses dados serão organizados. Ao ver que o espaço de memória tem 40 bytes, e o ponteiro que irá receber o endereço desses espaço foi declarado como um ponteiro de inteiros, o compilador irá dividir 4 bytes para cada endereçamento a partir do *v até completar os 40 bytes especificados.

Essa dúvida surgiu pois sempre que uso malloc nos meus exercícios, uso para alocar vetores dinâmicamente. E sempre fiquei na dúvida como o o espaço de memória que eu pedia retornaria com a distribuição correta dos elementos do vetor. Para que depois eu pudesse acessar v[1], v[4]… e assim vai.

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:

1 curtida