Memória, funções e objetos em C

Duração dos objetos, stack frames, propriedade da memória e apontadores para funções.

TLDR

Perguntar sobre esta página

ChatGPTClaudePerplexityGrokGeminiCopiar e abrirDeepSeekCopiar e abrir

Uma função pode receber o endereço de uma posição e alterar as suas coordenadas. Esse endereço só permite acesso enquanto o objeto existir. Se a função devolver o endereço de uma variável local, o chamador recebe um endereço cujo objeto já deixou de existir. Distinguir âmbito, duração e propriedade evita este erro e permite construir objetos que sobrevivem a várias chamadas.

Organização da memória

Um processo tem um espaço de endereçamento virtual. Uma organização habitual separa código executável, dados globais e estáticos, memória de alocação dinâmica e pilhas das chamadas. Esta organização não fixa endereços universais nem uma posição obrigatória para cada variável: o compilador pode guardar valores em registos e eliminar objetos que não precisam de existir na memória.

ObjetoQuando existeQuem controla a duração
Variável automática de um blocoDa entrada no bloco até à saídaA execução do bloco
Global ou staticDurante a execução do programaO programa e o ambiente
Memória de mallocDa alocação bem-sucedida até freeO código que a possui

Âmbito é onde podes usar um nome no código. Duração é durante quanto tempo o objeto existe. Uma variável static dentro de uma função tem âmbito local, mas conserva o valor entre chamadas.

Passagem de parâmetros

C passa os argumentos por valor. Uma função que recebe um apontador recebe uma cópia do endereço; desreferenciar essa cópia permite alterar o objeto original.

#include <stdio.h>

static int by_value(int x) { x = 9; return x; }
static void by_pointer(int *x) { *x = 9; }

int main(void) {
  int n = 3;
  by_value(n);
  printf("%d\n", n);
  by_pointer(&n);
  printf("%d\n", n);
  return 0;
}

Segue os dois casos. Em by_value(n), a função recebe 3, altera a sua cópia para 9 e devolve 9, mas o chamador ignora o retorno e n continua 3. Em by_pointer(&n), a função recebe o endereço de n, escreve 9 através dele e o chamador lê 9 a seguir. O apontador também passa por valor: a função recebe uma cópia do endereço, mas essa cópia continua a designar o objeto do chamador.

A mesma distinção explica por que p = NULL dentro de uma função que recebe int *p não põe a variável do chamador a NULL. Para substituir o apontador do chamador, devolve o novo apontador ou recebe o endereço desse apontador, de tipo int **.

Chamada, stack frame e retorno

Uma chamada guarda o endereço onde o chamador continuará depois do retorno. No modelo IA-32 habitual com argumentos na pilha, cada chamada cria um stack frame.

O frame pointer pode servir de referência fixa para encontrar os argumentos e variáveis locais, enquanto o stack pointer acompanha a ocupação da pilha. O código chamado conserva os registos que a convenção exige e coloca o resultado no local definido por essa convenção. No retorno, desfaz o frame e continua no endereço guardado.

Este é um modelo da convenção de chamada, não uma exigência de C. Noutras arquiteturas ou com otimização, alguns argumentos vão em registos e o frame pointer pode não existir. Em recursão, cada chamada tem os seus próprios objetos automáticos: a variável local da chamada exterior não é a da chamada interior.

Nunca devolvas o endereço de uma variável automática:

/* Incorreto: o objeto local deixa de existir quando a função retorna. */
int *bad_result(void) {
  int result = 42;
  return &result;
}

Mesmo que um teste imprima 42, o acesso já não é válido, porque o objeto local deixou de existir. Para devolver um inteiro, devolve o inteiro. Para produzir vários resultados, usa uma struct por valor ou recebe um apontador para armazenamento que o chamador possui.

Alocação e propriedade

malloc devolve armazenamento sem inicialização ou NULL se falhar. Calcula o tamanho a partir do objeto apontado, valida a falha, inicializa antes de ler e liberta exatamente uma vez. Depois de free, não leias nem escrevas o objeto. O valor dos apontadores que o designavam fica indeterminado; não os uses sequer para testar se a memória ainda existe. Podes atribuir NULL à variável do apontador sem ler o seu valor anterior, mas isso não altera outros aliases.

Este exemplo completo cria cinco inteiros e imprime soma=150, porque soma 10, 20, 30, 40 e 50 antes de libertar o vetor.

Alocar e libertar um vetor
#include <stdio.h>
#include <stdlib.h>

int main(void) {
  size_t n = 5;
  int *values = malloc(n * sizeof *values);
  if (values == NULL) return 1;
  int sum = 0;
  for (size_t i = 0; i < n; ++i) {
    values[i] = (int)(i + 1) * 10;
    sum += values[i];
  }
  printf("soma=%d\n", sum);
  free(values);
  values = NULL;
  return 0;
}
Resultado

Executa o código para ver a saída e os gráficos.

Para tamanhos recebidos de fora, confere também que n * sizeof *values não transborda SIZE_MAX. Um cast ao resultado de malloc é desnecessário em C quando incluis <stdlib.h>.

A propriedade responde a duas perguntas: quem pode usar o objeto e quem o deve libertar? Por exemplo, um sprite pode possuir o seu pixmap. Destruir o sprite liberta os dois. Se vários sprites partilham um pixmap, cada um libertá-lo causaria uma dupla libertação. Nesse caso, o módulo de recursos mantém o pixmap e os sprites apenas o usam.

Para aumentar uma alocação, realloc pode mover os dados para outro endereço. Guarda primeiro o resultado numa variável temporária. Se falhar para um tamanho não nulo, devolve NULL e a alocação anterior continua válida. Fazer values = realloc(values, size) perde o único endereço da alocação antiga se a chamada falhar. Se tiver sucesso, usa apenas o endereço devolvido; os apontadores anteriores, incluindo os que apontavam para elementos, deixam de ser válidos. Valida o produto do tamanho antes da chamada e trata o tamanho zero separadamente.

Num construtor com várias alocações, uma falha na segunda obriga a libertar a primeira. Não publiques um objeto parcialmente inicializado. A regra também vale para recursos de hardware: se a subscrição do rato falhar depois da do timer, remove a subscrição do timer antes de sair.

Estruturas, unions e alinhamento

Uma struct reserva espaço para todos os campos. O compilador pode acrescentar bytes de alinhamento entre campos e no fim. Por isso, a soma dos tamanhos dos campos pode ser menor do que sizeof(struct).

Uma union partilha armazenamento entre alternativas. Num valor com alternativas, guarda também uma etiqueta que indique qual delas está ativa:

enum event_kind { KEY_EVENT, MOVE_EVENT };
struct event {
  enum event_kind kind;
  union {
    unsigned key;
    struct { int x, y; } move;
  } data;
};

Se kind == MOVE_EVENT, interpreta data.move. Não uses data.key para descobrir automaticamente a alternativa. Esta estrutura chama-se tagged union e evita reservar os dados de todos os eventos ao mesmo tempo.

Uma estrutura de um protocolo ou da BIOS pode exigir uma disposição exata. Nesses casos, o apoio da cadeira usa estruturas empacotadas. Isso pode criar campos desalinhados e é dependente do compilador. Não envies uma struct C crua pela UART: padding, endianness e tamanhos podem mudar. Serializa cada campo num formato definido.

Calcular o tamanho com alinhamento

Suponhamos uma ABI em que char tem tamanho e alinhamento 1, short tem tamanho e alinhamento 2, e int tem tamanho e alinhamento 4. São hipóteses explícitas do exemplo; dizer apenas «máquina de 32 bits» não fixa todas as regras de C.

struct separados { char texto[10]; int numero; short codigo; };
union alternativas { char texto[10]; int numero; short codigo; };

Na estrutura, o array ocupa os offsets 0 a 9. O inteiro precisa de começar num múltiplo de 4, por isso ficam dois bytes de padding e numero começa em 12. Depois dos seus quatro bytes, codigo começa em 16 e termina em 17. O alinhamento da estrutura é 4; o tamanho tem de permitir que o objeto seguinte num array comece também alinhado. Dois bytes finais levam o total a 20.

Campo ou espaçoOffsetBytes
texto010
Padding antes de numero102
numero124
codigo162
Padding final182

Na union, as alternativas começam no offset 0 e partilham armazenamento. A maior exige 10 bytes, mas o alinhamento é 4: arredondar o tamanho para um múltiplo de 4 dá 12, não 10 nem a soma 16. Três unions num array ocupam 36 bytes; três estruturas ocupam 60.

Confere a ABI concreta com sizeof, _Alignof e offsetof, de <stddef.h>. Não uses este cálculo para serializar diretamente uma estrutura ou adivinhar o formato de um registo: o contrato externo pode exigir outra disposição.

Filas e tipos genéricos

Uma fila conserva a ordem de chegada. Numa fila circular com capacidade CC, o índice de entrada aponta para o próximo lugar onde escrever, o de saída aponta para o próximo elemento e a quantidade distingue cheio de vazio. Depois de avançar, usa índice módulo CC.

Com capacidade 3, começa em in = out = count = 0. Enfileirar A e B coloca-os nas posições 0 e 1. Retirar A deixa out = 1 e count = 1. Enfileirar C usa a posição 2 e faz in voltar a 0. Enfileirar D usa a posição 0. A fila está cheia e a ordem de saída é B, C, D, apesar de a ordem física no array ser D, B, C.

Segue a fila a partir de out, não do início do array. Quando in = out, é count que indica se está vazia ou cheia. Na figura, «livre» significa que a posição não pertence à fila; retirar um elemento não apaga os seus bytes.

Para guardar valores de diferentes tipos, conserva o tamanho de um elemento e copia bytes. Um void * não permite aritmética de apontadores no C padrão, porque void não tem tamanho. Usa um apontador para unsigned char na representação interna e calcula index * element_size.

Este programa completo guarda dois pares de coordenadas numa fila genérica de capacidade 3. A fila copia os valores, não conserva apontadores para as variáveis locais de quem insere, por isso imprime 2,3 e 5,8.

#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

struct queue {
  unsigned char *bytes;
  size_t capacity, element_size, in, out, count;
};

static struct queue *queue_new(size_t capacity, size_t element_size) {
  if (capacity == 0 || element_size == 0 || capacity > SIZE_MAX / element_size)
    return NULL;
  struct queue *q = malloc(sizeof *q);
  if (q == NULL) return NULL;
  q->bytes = malloc(capacity * element_size);
  if (q->bytes == NULL) { free(q); return NULL; }
  q->capacity = capacity;
  q->element_size = element_size;
  q->in = q->out = q->count = 0;
  return q;
}

static bool queue_put(struct queue *q, const void *value) {
  if (q->count == q->capacity) return false;
  memcpy(q->bytes + q->in * q->element_size, value, q->element_size);
  q->in = (q->in + 1) % q->capacity;
  ++q->count;
  return true;
}

static bool queue_get(struct queue *q, void *value) {
  if (q->count == 0) return false;
  memcpy(value, q->bytes + q->out * q->element_size, q->element_size);
  q->out = (q->out + 1) % q->capacity;
  --q->count;
  return true;
}

static void queue_delete(struct queue *q) {
  if (q == NULL) return;
  free(q->bytes);
  free(q);
}

int main(void) {
  struct point { int x, y; } a = {2, 3}, b = {5, 8}, value;
  struct queue *q = queue_new(3, sizeof a);
  if (q == NULL) return 1;
  if (!queue_put(q, &a) || !queue_put(q, &b)) {
    queue_delete(q);
    return 1;
  }
  while (queue_get(q, &value)) printf("%d,%d\n", value.x, value.y);
  queue_delete(q);
  return 0;
}

O chamador tem de fornecer objetos com pelo menos element_size bytes, porque a fila copia bytes sem conhecer o tipo. A cópia é superficial: se um elemento contiver um apontador, copia o endereço, não o objeto apontado. Define separadamente quem possui esse objeto.

Uma API de biblioteca pode ocultar struct queue no .c e expor apenas o tipo incompleto no .h. Uma fila com crescimento dinâmico exige ainda tratar falhas de realocação e reconstruir a ordem quando os elementos atravessam o fim do array. Um realloc bem-sucedido conserva bytes, mas não reorganiza sozinho uma fila circular. O exemplo usa capacidade fixa e devolve falha quando fica cheia.

Apontadores para funções

int (*operation)(int) declara um apontador para uma função que recebe um int e devolve um int. Os parênteses são necessários: int *operation(int) declararia uma função que devolve um apontador.

#include <stdio.h>

static int twice(int n) { return 2 * n; }
static int increment(int n) { return n + 1; }

int main(void) {
  int (*operations[])(int) = {twice, increment};
  for (size_t i = 0; i < 2; ++i)
    printf("%d\n", operations[i](4));
  return 0;
}

A saída mostra 8 e 5, porque a tabela seleciona twice(4) e depois increment(4). Confere o índice antes de aceder e mantém todas as funções com a assinatura declarada. Converter apontadores incompatíveis não adapta os argumentos.

Um tipo opaco permite esconder os campos de um objeto: o cabeçalho declara struct sprite;, expõe operações com struct sprite * e o .c define os campos. O utilizador não precisa do tamanho interno para conservar o apontador. Precisa de conhecer o contrato de criação, uso e destruição.

Argumentos variáveis

Uma função com ... não recebe informação automática sobre o número e o tipo dos argumentos. printf usa a cadeia de formato; um construtor de animações pode receber uma contagem explícita. A leitura usa va_list, va_start, va_arg e va_end de <stdarg.h>.

Os argumentos variáveis sofrem promoções: float passa como double e tipos inteiros pequenos passam normalmente como int. Pedir va_arg(args, float) para um float recebido assim é errado. Quando não precisas de tipos variáveis, um array mais um comprimento costuma ter um contrato mais fácil de conferir.

Exercícios

Libertação e cópia de apontadores

Uma função recebe int *p, chama free(p) e depois faz p = NULL. O chamador passou a sua variável values. Depois da chamada, pode testar values != NULL para saber se a memória ainda existe? Justifica e propõe uma interface adequada.

Pista
A função recebe uma cópia do valor de values.
Mais uma pista
Alterar p não altera o objeto values do chamador.
Solução

Não. free termina a duração do objeto e deixa indeterminado o valor dos apontadores que o designavam. Comparar esse valor com NULL não é uma verificação válida em C portátil. O chamador pode fazer values = destroy(values), com a função a devolver NULL, ou a função pode receber int ** e alterar *p depois da libertação. Outros aliases continuam a exigir uma política de propriedade.

Confere a tua resposta:

  • Expliquei a cópia do apontador.
  • Identifiquei o valor indeterminado depois de free.
  • Propus retorno de NULL ou parâmetro duplo.

Erros frequentes

Um apontador não se torna automaticamente NULL quando o objeto é libertado. Pôr um alias a NULL também não corrige os restantes.

Voltar à explicação
Devolver um resultado válido

Uma função cria int result = 17 e devolve &result, ou seja devolve o endereço de um objeto local. Num teste, o chamador imprime 17. Esse resultado prova que a função está correta? Dá duas alternativas sem esse erro de duração.

Pista

A memória pode ainda conter os mesmos bits depois do retorno.

Solução

O objeto automático deixa de existir no retorno. Desreferenciar o endereço devolvido é inválido, mesmo que os bits ainda mostrem 17. Devolve o inteiro por valor ou recebe um apontador para um int que o chamador conserva durante toda a chamada e escreve nesse objeto.

Confere a tua resposta:

  • Identifiquei o fim da duração do objeto.
  • Não usei um teste isolado como prova de validade.
  • Propus retorno por valor ou armazenamento do chamador.

Erros frequentes

Declarar result como apontador não prolonga a duração de um objeto automático. Usar static mudaria o contrato para armazenamento partilhado entre chamadas.

Voltar à explicação
Despacho por tabela e índice inválido

Uma tabela int (*table[])(int) = {f, g} contém f(n) = 3*n e g(n) = n+7. O índice vem de um byte recebido.

  1. Que resultado dá table[1](4)?
  2. O byte recebido vale 2. Que validação impede uma chamada inválida?
  3. Seria correto acrescentar uma função void h(void) através de um cast para int (*)(int)? Justifica.

Pista
Separa a escolha da entrada do contrato da função escolhida.
Solução

O índice 1 escolhe g, que devolve 4+7=11.

A tabela tem apenas os índices 0 e 1. Confere index < sizeof table / sizeof table[0] antes de avaliar table[index]; para 2, rejeita a entrada sem chamar nenhuma função.

O cast não cria um adaptador. Chamar uma função com um tipo incompatível causa comportamento indefinido. Usa funções com a assinatura declarada ou uma função adaptadora com essa assinatura e um contrato de retorno definido.

Confere a tua resposta:

  • Obtive 11 na chamada válida ao índice 1.
  • Rejeitei o índice 2 antes de aceder à tabela.
  • Expliquei por que um cast não adapta assinaturas de funções.
Voltar à explicação
Seguir uma fila cheia

Uma fila de capacidade 3 começa vazia. Insere 10, 20 e 30, retira um elemento e insere 40. Dá os índices in e out, a quantidade e a ordem das próximas saídas. Uma nova inserção deve ter sucesso?

Pista
Cada índice avança módulo 3.
Solução

Depois das três inserções, in = out = 0 e count = 3. Retirar 10 faz out = 1 e count = 2. Inserir 40 usa a posição 0 e faz in = 1 e count = 3. As saídas são 20, 30 e 40. A fila está cheia; uma nova inserção falha sem alterar dados nem índices.

Confere a tua resposta:

  • Obtive in = out = 1 e count = 3.
  • Mantive a ordem 20, 30, 40.
  • Rejeitei inserir numa fila cheia sem perder dados.

Erros frequentes

in = out também ocorre quando a fila está cheia. Sem count ou outra convenção, os índices não distinguem os dois casos.

Voltar à explicação
Conservar uma alocação na falha

values é o único apontador para uma alocação válida. A instrução values = realloc(values, 200) devolve NULL. O bloco antigo foi libertado? Explica o problema e uma sequência segura, sem executar uma falha deliberada de memória.

Pista
O tamanho pedido é não nulo.
Solução

O bloco antigo continua alocado. A atribuição perde o seu único endereço e cria uma fuga de memória. Usa int *next = realloc(values, 200); se next for NULL, conserva values e trata o erro; caso contrário, atribui values = next. Depois do sucesso, não uses aliases do endereço anterior. Os 200 são bytes, não 200 inteiros.

Confere a tua resposta:

  • Mantive o bloco antigo alocado na falha.
  • Usei um apontador temporário.
  • Distingui tamanho em bytes e invalidação de aliases no sucesso.

Erros frequentes

Libertar values depois da atribuição falhada chama free(NULL) e não recupera o bloco perdido. Não confundir falha com o sucesso que pode mover o bloco.

Voltar à explicação
Duração de um recurso partilhado

O módulo de recursos possui um pixmap alocado dinamicamente. Dois elementos de uma fila guardam estruturas com um apontador para esse pixmap; a fila copia os bytes de cada estrutura. Os elementos usam o pixmap sem adquirir a sua propriedade.

O primeiro elemento é retirado e destruído. Deve libertar o pixmap? Quando pode o módulo de recursos libertá-lo? Explica também por que copiar a estrutura não cria duas imagens independentes.

Pista
Identifica o objeto copiado e o objeto apenas referido.
Solução

A fila copia as estruturas, incluindo o valor do apontador. O pixmap continua a ser um único objeto partilhado.

Destruir o primeiro elemento não liberta o pixmap: o segundo ainda o usa e nenhum dos dois o possui. O módulo de recursos liberta-o uma única vez, depois de terminar todos os seus utilizadores, incluindo os elementos ainda na fila. Libertá-lo antes deixaria apontadores inválidos; libertá-lo em cada elemento causaria dupla libertação.

Confere a tua resposta:

  • Reconheci que as cópias partilham o mesmo pixmap.
  • Mantive o pixmap vivo enquanto algum elemento da fila o utiliza.
  • Atribuí uma única libertação ao módulo de recursos.
Voltar à explicação
Maior membro não é sempre o tamanho da union

Assume uma ABI com tamanho/alinhamento de char igual a 1/1, short 2/2 e int 4/4. O alinhamento de uma struct ou union é o maior dos alinhamentos dos membros, e o tamanho é arredondado para esse alinhamento.

struct S { char texto[10]; int numero; short codigo; };
union U { char texto[10]; int numero; short codigo; };

Calcula os offsets dos campos e sizeof S, sizeof U e sizeof de um array com três U. Num array de S, a quantos bytes do início fica numero do segundo elemento? Dizer apenas «arquitetura de 32 bits» chegaria para justificar estas contas?

Solução

Na struct, texto começa em 0 e ocupa dez bytes. Numero começa em 12 para respeitar alinhamento 4 e ocupa quatro; codigo começa em 16 e ocupa dois. A estrutura termina os campos em 18, mas precisa de dois bytes finais: sizeof S é 20.

Na union, todos os membros começam em 0. O maior ocupa dez bytes, arredondados ao alinhamento quatro: sizeof U é 12. Três elementos ocupam 36 bytes. O segundo S começa no offset 20 e o seu numero em 20 + 12 = 32.

As hipóteses da ABI determinam estes resultados. O número de bits do endereço, por si só, não fixa tamanhos nem alinhamentos de todos os tipos. Confere sizeof, _Alignof e offsetof no ambiente concreto sem os confundir com um formato externo empacotado.

Confere a tua resposta:

  • Calculei offsets 0, 12 e 16 e tamanho 20 para a struct.
  • Arredondei o maior membro 10 ao alinhamento 4, obtendo union de 12.
  • Obtive 36 bytes para três unions e offset 32 para numero no segundo elemento da struct.
Voltar à explicação

À tua maneira

Aparência

Modo de cor
Tema
Cor de destaque

Leitura

Fonte de leitura

Álgebra, lógica e uma ideia de cada vez.

Código

Fonte do código

Navegação

Cursor de leitura, sem edição. h, j, k, l movem; / pesquisa nesta página. v seleciona caracteres; V seleciona linhas; y copia. Tab permite destacar ou comentar. Espaço ou Enter abre blocos recolhíveis. Nos cartões, h, j, k, l mudam o foco.

CSS personalizado

Ativa uma sugestão ou guarda o teu CSS. Só muda este navegador.

Usa --page, --text e --accent para acompanhar o tema.

Seletores e recuperação

.site-header, .prose, .course-card, [data-reading-history], [data-playground] e .page-actions.

Abre a aparência com a tecla vírgula ou no rodapé. Se o CSS esconder os controlos, abre esta página sem CSS personalizado.

Pesquisar

Escreve para pesquisar em todo o site.

para escolher · Enter para abrir · Esc para fechar

Copiar fórmula

A cópia automática não está disponível. Copia o LaTeX abaixo.

Pesquisar nesta página

Enter procura · n / N repetem · Esc cancela. Ctrl / Cmd K pesquisa todo o site.

Atalhos de teclado

Clica numa tecla para a mudar. Esc cancela. Backspace desativa.

PesquisarCtrl / Cmd K

Os atalhos não interferem enquanto escreves. Tab e Enter funcionam sempre.