Menu

Ponteiro para struct em C: o operador seta e structs no heap

Como apontar para uma struct em C - o operador seta, por que (*p).x precisa dos parênteses, passar structs por ponteiro para alterá-las ou evitar cópias, alocar uma struct com malloc e montar um nó de lista encadeada.

Esta página tem editores executáveis - edite, execute e veja a saída na hora.

Uma struct passada a uma função é copiada. Isso tudo bem para um ponto de dois ints e é desperdício para um registro de 200 bytes - e torna a alteração impossível, porque a função só enxerga a própria cópia. Apontar para uma struct resolve os dois problemas de uma vez, e é também a única maneira de construir estruturas que crescem: listas, árvores, grafos.

Pegando um ponteiro para uma struct

Nada de especial é necessário. O & dá o endereço, e o tipo é "ponteiro para struct":

Os três imprimem 3. Os dois últimos são a mesma operação escrita de duas formas.

Por que (*ptr).x precisa dos parênteses

Você pode tentar tirá-los e escrever *ptr.x. Isso compila para algo bem diferente ou, com mais frequência, se recusa a compilar - porque o operador de membro . tem precedência mais alta que a desreferência *:

*ptr.x        // interpretado como *(ptr.x)  -- errado
(*ptr).x      // desreferencia primeiro, depois pega o membro -- correto
ptr->x        // a mesma coisa, dita de forma curta

ptr.x pede um membro do ponteiro, e um ponteiro não tem membros. A reclamação do compilador ("request for member 'x' in something not a structure or union") é um bug de precedência disfarçado.

Como essa forma com parênteses é feia e fácil de errar, o C fornece o ->. Na prática você quase nunca verá (*p).x em código real; recorra a p->x e esqueça que a forma longa existe, exceto como explicação do que a seta significa.

Passando uma struct por ponteiro

Um parâmetro ponteiro dá à função a struct real do chamador, então as mudanças permanecem:

Dois hábitos que vale formar aqui:

  • Marque ponteiros somente leitura como const. const struct Point *p promete que a função não vai mudar aquilo para que p aponta. Se alguém depois acrescentar p->x = 0 lá dentro, a compilação falha em vez de o bug ir para produção. Também documenta a intenção no ponto de chamada.
  • Passe um ponteiro para structs grandes mesmo ao apenas ler. Uma struct com um buffer de nome de 64 bytes custa 64 bytes de cópia por chamada por valor; um ponteiro custa 8.

Structs pequenas ainda podem ser passadas por valor - uma struct Point de 8 bytes copia tão barato quanto um ponteiro, e a versão por valor não pode ser NULL, o que elimina um modo de falha inteiro.

Ponteiros dentro de arrays de structs

A aritmética de ponteiros funciona em arrays de struct exatamente como em arrays de int: p + 1 avança uma struct inteira, incluindo o preenchimento.

O nome de um array decai para um ponteiro ao seu primeiro elemento, então staff já é um struct Employee * na chamada. É também por isso que a função precisa receber n à parte - o sizeof dentro da função mediria o ponteiro, não o array.

Alocando uma struct no heap

Structs na pilha morrem no fim do seu escopo. Para fazer uma que sobreviva à função que a criou - ou para fazer quantas a entrada exigir - aloque com malloc:

Quatro detalhes dessa pequena função são todos essenciais:

  • sizeof *e, não sizeof(struct Employee). Isso se lê como "o tamanho daquilo para que e aponta", então, se o tipo mudar algum dia, a alocação acompanha automaticamente. Não há como os dois se desencontrarem.
  • Verifique o NULL. O malloc devolve NULL quando não consegue atender ao pedido. Escrever e->id por um ponteiro nulo é uma falha de segmentação.
  • Inicialize todos os membros. O malloc não zera a memória; a struct chega cheia de lixo. (calloc(1, sizeof *e) a zera para você.)
  • Alguém precisa chamar free. Devolver um ponteiro alocado transfere esse dever ao chamador. Diga isso num comentário - uma regra de propriedade pouco clara é como começam os vazamentos de memória.

A recompensa: um nó de lista encadeada

Eis o que structs não conseguem fazer sem ponteiros. Uma struct não pode conter a si mesma - isso seria infinitamente grande - mas pode conter um ponteiro para uma da sua própria espécie, e esse único truque constrói toda estrutura de dados encadeada em C.

Repare que struct Node *next; se refere a struct Node enquanto struct Node ainda está sendo declarada. Isso é legal justamente porque um ponteiro tem tamanho conhecido independentemente daquilo para que aponta - o compilador ainda não precisa da definição completa. É também o único lugar em que você precisa usar a tag struct Node mesmo tendo escrito um typedef: o nome do typedef ainda não existe dentro da própria definição.

O laço de free_list salva head->next antes de chamar free(head). Ler head->next depois da liberação é um uso-após-liberação - a memória não é mais sua, e o valor lido é o que o alocador tiver colocado ali.

Erros comuns

  • p->x num ponteiro não inicializado ou NULL. A causa mais comum de travamento em código cheio de structs. Inicialize ponteiros com NULL e verifique antes de desreferenciar.
  • Devolver um ponteiro para uma struct local. struct Point *bad(void) { struct Point p = {1,2}; return &p; } devolve o endereço de memória que deixa de existir no instante em que a função retorna. Devolva a struct por valor, ou aloque-a.
  • Liberar duas vezes, ou esquecer de liberar. Cada malloc casa com exatamente um free. Depois de liberar, coloque NULL no ponteiro para que um uso acidental posterior trave alto em vez de corromper a memória em silêncio.
  • Copiar uma struct que contém um ponteiro. A atribuição de struct copia o valor do ponteiro, não aquilo para que ele aponta. As duas structs agora referenciam o mesmo buffer, e a primeira que o liberar deixa a outra pendurada.

Perguntas frequentes

O que o operador -> faz em C?

p->x acessa o membro x através do ponteiro p. É exatamente equivalente a (*p).x - desreferenciar o ponteiro e depois pegar o membro. A seta existe porque ponteiros para structs são tão comuns que a forma longa polui o código.

Por que *p.x não funciona com um ponteiro de struct?

Porque o . liga mais forte que o *. *p.x é interpretado como *(p.x) - pegar o membro x de p (que é um ponteiro, não uma struct) e desreferenciar isso. Você precisa de (*p).x, ou simplesmente escreva p->x.

Devo passar uma struct por valor ou por ponteiro em C?

Passe por ponteiro quando a função precisa modificar a struct do chamador, ou quando a struct é grande e copiá-la seria desperdício. Passe por valor para structs pequenas e somente leitura, em que a cópia é barata e o código é mais simples. Marque parâmetros de ponteiro somente leitura como const struct T * para que o compilador imponha a promessa.

Como alocar uma struct com malloc em C?

struct Node *n = malloc(sizeof *n); - a forma sizeof *n pede o tamanho daquilo para que n aponta, então continua correta se você mudar o tipo depois. Verifique o resultado contra NULL, inicialize todos os membros e chame free(n) ao terminar.

Coddy programming languages illustration

Aprenda a programar com o Coddy

COMEÇAR