Gerenciamento de memória dinâmica em C

Aprenda como um programa em C obtém memória durante sua execução e a devolve. Compare a pilha e o heap, aloque blocos com malloc e calloc, redimensione-os com realloc e libere-os com free. Reconheça vazamentos, ponteiros pendentes, liberações duplas e acessos fora dos limites, compreenda por que eles causam encerramentos inesperados, como falhas de segmentação, e encontre e corrija esses problemas com Valgrind e AddressSanitizer.

BásicoAula 5 de 8 do curso Curso de Programação em C

O que você vai aprender

  1. Descreva a pilha, o heap e a memória estática, por quanto tempo os dados existem em cada um e quando um programa precisa de memória no heap.
    • Compare a pilha, o heap e o armazenamento estático
      Descreva como os armazenamentos automático, estático e alocado diferem quanto ao tempo de vida e a quem os gerencia, e o que acontece com cada um durante chamadas de função.
    • Determine quando os dados devem ser alocados no heap
      Escolha entre variáveis locais e alocação no heap com base no tamanho, no tempo de vida e no crescimento dos dados, e evite retornar o endereço de uma variável local.
  2. Reserve blocos do tamanho correto com malloc e calloc, verifique se cada solicitação foi bem-sucedida e libere cada bloco exatamente uma vez, com um responsável definido.
    • Solicite memória com malloc e sizeof
      Calcule a quantidade de bytes a solicitar, chame malloc, verifique se houve falha e use o bloco como um arranjo.
    • Obtenha memória preenchida com bits zero usando calloc
      Use calloc para alocar arranjos cujos elementos começam em zero e saiba quando ela é uma escolha melhor que malloc.
    • Libere memória com free e acompanhe a responsabilidade pela liberação
      Libere cada bloco exatamente uma vez no momento certo, determine qual parte do programa é responsável por cada bloco e libere os recursos corretamente quando uma alocação falhar.
  3. Aumente e reduza blocos no heap sem perder dados ou memória e implemente arranjos que se expandem à medida que os dados chegam.
    • Aumente e reduza um bloco com segurança usando realloc
      Receba o resultado de realloc em um ponteiro temporário, mantenha os dados e o bloco original seguros em caso de falha e atualize todos os ponteiros após a movimentação do bloco.
    • Implemente um arranjo que possa crescer
      Mantenha um comprimento e uma capacidade junto ao ponteiro, aumente a capacidade geometricamente e leia uma entrada de comprimento desconhecido em um buffer que se expande.
  4. Identifique vazamentos, ponteiros pendentes, liberações duplas, acessos fora dos limites e leituras de valores não inicializados em código C, explique por que são perigosos e rastreie uma falha de segmentação até sua causa.
    • Encontre e evite vazamentos de memória
      Explique o que é um vazamento, identifique os padrões de código que o causam e adote hábitos que o evitem.
    • Evite ponteiros pendentes, uso após a liberação e liberações duplas
      Reconheça ponteiros para memória cujo tempo de vida terminou, explique por que usá-los ou liberá-los novamente é comportamento indefinido e adote hábitos que evitem esses erros.
    • Respeite os limites do bloco e evite estouros e leituras de valores não inicializados
      Mantenha cada acesso dentro dos limites do bloco, dimensione corretamente os buffers de cadeias de caracteres e atribua valores à memória antes de lê-la.
    • Explique e rastreie falhas de segmentação
      Explique o que é uma falha de segmentação, indique suas causas comuns e parta do encerramento inesperado para chegar à linha com defeito.
  5. Execute programas no Memcheck do Valgrind e no AddressSanitizer, leia seus relatórios e use-os para corrigir vazamentos e acessos inválidos até que um programa execute sem erros.
    • Verifique um programa com o Memcheck do Valgrind
      Compile um programa para o Valgrind, execute-o com verificação de vazamentos e leia suas mensagens de erro, seu resumo do heap e suas categorias de vazamento.
    • Use AddressSanitizer e LeakSanitizer
      Compile um programa com AddressSanitizer, leia seus relatórios e escolha entre sanitizadores e Valgrind.
    • Corrija defeitos de memória com base no relatório de uma ferramenta
      Siga um processo reproduzível desde o relatório de uma ferramenta até a linha com defeito, corrija a causa em vez do sintoma e confirme a correção com uma execução sem erros.

Guia de estudo de Gerenciamento de memória dinâmica em C

5 capítulos

1

Explique onde um programa em C armazena seus dados

Todo objeto tem um tempo de vida, e um ponteiro só é útil enquanto o objeto para o qual aponta existe. Variáveis locais, dados estáticos e blocos alocados seguem regras diferentes de tempo de vida. Relacionar essas regras à pilha e ao heap explica por que uma função pode retornar um arranjo alocado, mas não pode retornar um endereço utilizável de seu próprio arranjo local. O tamanho, o tempo de vida e o crescimento posterior orientam a escolha do armazenamento para cada tarefa.

1.1 Armazenamento e tempo de vida

O endereço de uma variável informa onde seus dados estão. Sua duração de armazenamento informa por quanto tempo esses dados existem. Guardar um endereço não mantém o objeto vivo.

Três tempos de vida
Duração de armazenamentoObjetos típicosFim do tempo de vida
AutomáticaVariáveis locais comuns e parâmetrosQuando seu bloco termina
EstáticaVariáveis globais e locais declaradas staticQuando o programa termina
AlocadaBlocos obtidos de funções de alocaçãoQuando o bloco é desalocado

Em sistemas comuns, objetos automáticos usam a pilha de chamadas, e blocos alocados usam o heap. C define os tempos de vida; não exige essas regiões físicas. O código de máquina e os dados estáticos ocupam outras regiões, cuja disposição depende do sistema.

Uma chamada de função normalmente cria um registro de ativação que contém seus parâmetros, variáveis locais e informações de retorno. As chamadas se aninham: o registro mais recente é removido primeiro. O retorno libera o armazenamento automático daquela chamada, então chamadas posteriores podem reutilizar o espaço. O alocador do heap gerencia uma área separada. Ele reserva um bloco contíguo mediante solicitação e fornece seu endereço inicial. Retornar de uma função não libera esse bloco.

O ponteiro e seu bloco são objetos separados

Considere esta função, declarada com <stdlib.h> disponível:

int *make_array(size_t n) {
    if (n == 0 || n > SIZE_MAX / sizeof(int)) return NULL;
    int *p = malloc(n * sizeof *p);
    if (p == NULL) return NULL;
    return p;
}

SIZE_MAX vem de <stdint.h>. Quando main chama int *nums = make_array(5);, o parâmetro n e a variável ponteiro p pertencem à chamada. O arranjo alocado não pertence. Quando um int ocupa 4 bytes, o bloco contém 20 bytes. O retorno copia seu endereço para nums; não copia o arranjo. A função chamadora verifica nums, usa os elementos depois de atribuir valores a eles e, por fim, libera o bloco.

A variável local p deixa de existir quando a função retorna. O bloco permanece porque sua duração de armazenamento é alocada, não automática. Essa distinção também funciona no sentido inverso: um ponteiro local pode apontar para um objeto global, e um ponteiro global pode apontar para um bloco no heap.

Dados estáticos sobrevivem às chamadas
int next_id(void) {
    static int id = 0;
    return ++id;
}

Chamadas sucessivas retornam 1, 2 e 3. O inicializador executa uma vez, e id existe durante toda a execução do programa. Remover static cria uma nova variável automática id, inicializada em zero, a cada chamada; assim, cada chamada retorna 1.

O espaço da pilha é limitado

Um arranjo local grande e chamadas aninhadas em muitos níveis consomem espaço na pilha. Ultrapassar o limite causa um estouro de pilha, que geralmente encerra o programa. Muitas configurações do Linux permitem uma pilha de 8 MiB; a reserva padrão da pilha da thread principal da Microsoft é geralmente 1 MiB. Essas são configurações do sistema, não regras de C. Um arranjo local de 4.000.000 de valores int precisa de 16.000.000 bytes quando um int ocupa 4 bytes, cerca de 15,3 MiB.

A alocação no heap tem custos de controle ao reservar e liberar blocos. Ler um elemento não é inerentemente mais lento apenas porque seu arranjo está no heap. Além disso, a área de memória do heap não tem relação com a estrutura de dados heap usada em uma fila de prioridade.

Principais conclusões

  • A duração de armazenamento de um objeto determina seu tempo de vida.
  • Variáveis locais automáticas deixam de existir com seu bloco; objetos estáticos duram toda a execução do programa.
  • Um ponteiro local e o bloco no heap para o qual ele aponta têm tempos de vida separados.
  • Arranjos locais grandes e chamadas profundamente aninhadas podem esgotar a pilha.
  • C especifica tempos de vida, enquanto pilha e heap descrevem implementações comuns.

1.2 Escolha do armazenamento para a tarefa

Escolha o armazenamento perguntando quanto espaço é necessário, quando esse tamanho se torna conhecido e por quanto tempo os dados devem existir. A solução adequada mais simples costuma ser um arranjo local comum.

Escolha o armazenamento conforme os dados
NecessidadeEscolha inicial adequada
Um arranjo pequeno e fixo usado durante uma chamadaArranjo local
Uma quantidade lida da entradaAlocação no heap com verificação
Um resultado que sobreviva à função que o criaAlocação no heap ou buffer de responsabilidade da função chamadora
Dados que crescem à medida que a entrada chegaBuffer redimensionável no heap
Um arranjo muito grandeAlocação no heap com verificação de falha

As temperaturas diárias de uma semana precisam de sete valores, então double temps[7]; é um arranjo local natural. Uma nota por estudante exige um tamanho obtido em tempo de execução. Para n estudantes, use double *grades = malloc(n * sizeof *grades); depois de verificar o tamanho. Para 250 estudantes e valores double de 8 bytes, a solicitação é de 2.000 bytes. Depois, verifique o resultado antes de preencher o arranjo.

Por que retornar um buffer local falha
char *greeting(void) {
    char buf[16] = "hello";
    return buf;  /* error: buf ends when this call returns */
}

O endereço retornado não pode prolongar o tempo de vida de buf. A função chamadora recebe um ponteiro pendente, mesmo que os bytes antigos pareçam inalterados. GCC e Clang podem emitir avisos sobre esse padrão.

Uma correção é deixar a função chamadora fornecer o armazenamento:

#include <string.h>

void greeting(char out[16]) {
    strcpy(out, "hello");
}

/* In the caller: */
char message[16];
greeting(message);

Aqui, o arranjo da função chamadora permanece vivo durante toda a chamada e depois dela. O parâmetro ainda se comporta como um ponteiro; a função chamadora deve fornecer espaço suficiente. Uma segunda solução aloca um resultado no heap e torna a função chamadora responsável por liberá-lo. Uma terceira retorna um buffer estático. Esse buffer permanece, mas todas as funções chamadoras o compartilham, e uma chamada posterior pode sobrescrever seu conteúdo. Escolha com base na necessidade de resultados independentes pelas funções chamadoras.

Tamanho em tempo de execução nem sempre significa heap

O C99 introduziu arranjos de tamanho variável, como int a[n];. Eles têm armazenamento automático, geralmente na pilha. O C11 tornou o suporte opcional, e o compilador da Microsoft não oferece suporte a eles. Uma entrada grande pode esgotar a pilha, e a declaração do arranjo não fornece um resultado nulo a verificar. A alocação no heap oferece um caminho de falha verificável e permite o redimensionamento posterior.

Um ponteiro não indica responsabilidade pela liberação
int value = 3;
int *p = &value;

p aponta para um objeto automático. Passá-lo a free é inválido. A mesma regra impede a liberação de um arranjo estático ou de um literal de cadeia de caracteres. O endereço deve pertencer a uma alocação ainda válida pela qual o programa seja responsável.

Da mesma forma, char *s = "hello"; aponta para um literal de cadeia de caracteres; não cria uma cópia na qual se possa escrever. Modificar o literal é comportamento indefinido. Use char s[] = "hello"; para uma cópia local gravável ou aloque espaço para uma cópia separada no heap.

Principais conclusões

  • Escolha o armazenamento conforme o tamanho dos dados, o tempo de vida necessário e a necessidade de crescimento.
  • Um endereço retornado por uma função só é utilizável se o objeto para o qual aponta ainda existir.
  • Um buffer fornecido pela função chamadora mantém com ela a responsabilidade pela liberação.
  • Um buffer estático de resultado é compartilhado entre chamadas.
  • Ponteiros podem apontar para objetos locais, estáticos ou alocados; somente blocos alocados ainda válidos podem ser liberados.
  • Literais de cadeia de caracteres não são cópias graváveis.
2

Aloque e libere memória no heap

Uma solicitação de alocação precisa de uma contagem de bytes, um caminho para falhas e um responsável pela liberação. Comece pela quantidade de elementos para determinar o armazenamento necessário, decida se seus valores iniciais devem ser zero e use cada bloco somente após uma solicitação bem-sucedida. A responsabilidade pela liberação relaciona o último uso à liberação de recursos, incluindo alocações aninhadas e funções que só conseguem concluir parte do trabalho.

2.1 Solicitação da quantidade correta de bytes

malloc reserva um bloco contíguo de bytes e retorna seu endereço inicial. Inclua <stdlib.h> para obter sua declaração. Uma alocação bem-sucedida fornece armazenamento, mas não o preenche com valores úteis.

Conte os elementos e determine o tamanho de cada um
int *nums = malloc(n * sizeof *nums);

n é uma quantidade de elementos; sizeof *nums é a quantidade de bytes por elemento. A multiplicação fornece a quantidade de bytes. Quando um int ocupa 4 bytes, dez valores int precisam de 40 bytes. Dez valores double de 8 bytes precisam de 80 bytes. Dez ponteiros precisam de 80 bytes quando cada ponteiro ocupa 8 bytes. Um char ocupa sempre um byte de C.

A forma sizeof *nums acompanha o tipo apontado se a declaração mudar. Para esse tipo comum, que não tem tamanho variável, sizeof não avalia *nums, então não desreferencia um ponteiro não inicializado. Em C, o void * retornado é convertido no tipo de ponteiro para objeto sem uma conversão explícita.

malloc(n) aloca n bytes, não n valores int. sizeof nums mede a variável ponteiro, não um elemento ou o bloco inteiro. Em um sistema com ponteiros de 8 bytes e valores int de 4 bytes, malloc(n * sizeof nums) solicita o dobro do espaço necessário, mascarando a expressão incorreta. Com outros tipos, pode alocar espaço insuficiente.

Verifique o tamanho e a falha antes do uso

Este programa completo rejeita uma solicitação vazia, protege a multiplicação, verifica a alocação e inicializa cada elemento:

#include <stdint.h>
#include <stdlib.h>

int main(void) {
    size_t n = 5;
    int *nums;
    if (n == 0 || n > SIZE_MAX / sizeof *nums) {
        return EXIT_FAILURE;
    }
    nums = malloc(n * sizeof *nums);
    if (nums == NULL) return EXIT_FAILURE;

    for (size_t i = 0; i < n; ++i) {
        nums[i] = (int)(i * i);
    }
    free(nums);
    return EXIT_SUCCESS;
}

SIZE_MAX, de <stdint.h>, é o maior valor de size_t. Verifique com divisão antes da multiplicação. Se o produto sofrer redução modular primeiro, verificar o resultado reduzido não pode recuperar o tamanho pretendido. Uma solicitação diferente de zero pode falhar mesmo quando seu cálculo cabe no tipo, então a verificação de ponteiro nulo é uma etapa separada.

Os valores inicializados aqui são 0, 1, 4, 9 e 16. nums[i] acessa o mesmo elemento que *(nums + i). A aritmética de ponteiros avança em elementos, então o código não multiplica i pelo tamanho em bytes.

Mantenha a quantidade junto ao ponteiro

Não há operação padrão em C que recupere a quantidade de elementos de uma alocação a partir de seu ponteiro. Armazene n separadamente e passe o ponteiro e a quantidade às funções. A quantidade controla os laços e as verificações de índice.

Uma cadeia copiada precisa de seus caracteres e do terminador '\0'. A cadeia “Amsterdam” tem nove caracteres e precisa de dez bytes, então solicite strlen(s) + 1 para sua cópia. <string.h> fornece strlen.

Uma solicitação a malloc de tamanho zero pode retornar um ponteiro nulo ou um ponteiro que pode ser liberado, mas não desreferenciado. Trate quantidades iguais a zero como um caso vazio explícito, em vez de tratar esse ponteiro como armazenamento utilizável para um arranjo.

Principais conclusões

  • Multiplique a quantidade de elementos por sizeof *pointer para obter a quantidade de bytes de um arranjo.
  • Verifique o estouro da multiplicação antes de chamar malloc.
  • Verifique se o resultado é NULL antes de usá-lo.
  • Inicialize cada elemento de malloc antes de lê-lo.
  • Guarde a quantidade de elementos separadamente; sizeof pointer não pode recuperá-la.
  • Inclua o caractere nulo terminador ao alocar uma cópia de cadeia de caracteres.
  • Trate explicitamente quantidades iguais a zero.

2.2 Inicialização de um arranjo em zero

Um contador precisa de um valor inicial definido antes do primeiro incremento. calloc combina alocação e preenchimento com zeros, então um arranjo de contadores inteiros está pronto para contar após a verificação de falha.

Dois argumentos descrevem o arranjo

Inclua <stdlib.h> e escreva:

int *tally = calloc(26, sizeof *tally);
if (tally == NULL) return EXIT_FAILURE;

O primeiro argumento é a quantidade de elementos e o segundo é o tamanho de um elemento. Quando um int ocupa 4 bytes, isso reserva 104 bytes. Cada byte começa em zero, então cada contador inteiro começa em zero. Chamar calloc com um único argumento combinado não compila; sua interface exige dois argumentos.

Para a palavra “banana”, incremente o contador correspondente a cada letra. Após processá-la, os contadores de a, b e n contêm 3, 1 e 2. Todos os outros permanecem em zero. Em uma codificação de caracteres com letras minúsculas consecutivas, a expressão do índice é c - 'a'. A entrada deve ser uma letra minúscula antes de essa expressão ser usada como índice de arranjo.

#include <stdlib.h>

int main(void) {
    int *counts = calloc(3, sizeof *counts);
    if (counts == NULL) return EXIT_FAILURE;
    counts[1]++;
    counts[1]++;
    /* The counters now hold 0, 2, 0. */
    free(counts);
    return EXIT_SUCCESS;
}

Apenas o contador do meio foi alterado pelo código de contagem. Seus dois vizinhos ainda têm valores úteis porque a alocação os preencheu com zeros.

O que significa preenchido com bits zero

calloc define todos os bits como zero. Isso fornece valores zero para elementos inteiros e char. Portanto, um buffer de caracteres de tamanho positivo preenchido com zeros começa como uma cadeia vazia: seu primeiro caractere é '\0'.

Nas plataformas mais usadas, bits todos iguais a zero também representam zero de ponto flutuante e ponteiros nulos. C portável não garante essas representações para tipos de ponto flutuante e ponteiros. Se o código precisar desses valores em todas as implementações de C, atribua 0.0 ou NULL explicitamente aos elementos.

Escolha entre as funções de alocação
Uso inicialEscolha natural
Incrementar contadores inteiroscalloc
Definir indicadores inteiros gradualmente a partir de um estado com todos os valores em zerocalloc
Sobrescrever imediatamente cada elementomalloc

Uma chamada bem-sucedida a malloc seguida de memset pode preencher um bloco com zeros, mas a multiplicação do tamanho nesse código ainda precisa de uma proteção contra estouro. As implementações mais usadas de calloc rejeitam um produto grande demais para size_t; o C23 exige isso explicitamente. O resultado ainda precisa de uma verificação de ponteiro nulo, pois uma solicitação de tamanho correto pode falhar.

O preenchimento com zeros se aplica a essa alocação. Se realloc aumentar o bloco depois, os bytes adicionados não serão inicializados. Atribua valores a esses elementos antes de incrementá-los ou lê-los. Por fim, libere o bloco com free, como faria com um bloco obtido com malloc.

Principais conclusões

  • Passe a calloc uma quantidade de elementos e um tamanho de elemento.
  • Contadores inteiros preenchidos com zeros podem ser incrementados imediatamente após a verificação da alocação.
  • Bytes com todos os bits em zero não substituem de forma portável a atribuição de zero de ponto flutuante ou de ponteiros nulos.
  • O crescimento com realloc não preenche os bytes adicionados com zeros.

2.3 Um responsável e uma liberação final

Um bloco alocado precisa de uma resposta clara para “Quem libera isto?”. Essa é a responsabilidade pela liberação. Outro código pode tomar o endereço emprestado sem receber permissão para liberá-lo.

Libere após o último uso

free, declarada em <stdlib.h>, torna um bloco alocado ainda válido disponível para reutilização. Passe seu endereço inicial exatamente uma vez. free(NULL) não faz nada. Passar um endereço local, um literal de cadeia de caracteres, um endereço interno como p + 1 ou um bloco já liberado causa comportamento indefinido.

free(p) não atribui um valor à variável p e não promete apagar os bytes. Após a liberação, o valor antigo do ponteiro é inválido. Se a variável continuar em uso, atribua p = NULL; sem examinar seu valor antigo. Isso altera apenas p; as referências alternativas ainda precisam deixar de usar o bloco liberado.

Uma função que retorna memória recém-alocada transfere a responsabilidade pela liberação à função chamadora:

#include <stdlib.h>
#include <string.h>

char *copy_string(const char *s) {
    char *copy = malloc(strlen(s) + 1);
    if (copy == NULL) return NULL;
    strcpy(copy, s);
    return copy;
}

A função chamadora verifica o resultado, usa a cópia e a libera. A cadeia de entrada é emprestada; essa função não a libera. POSIX e C23 oferecem strdup para o mesmo padrão de responsabilidade pela liberação; a função acima também funciona em C17.

Libere alocações aninhadas de dentro para fora

Uma grade feita de um arranjo de ponteiros para linhas tem blocos separados. Três ponteiros para linhas e três linhas de quatro valores int exigem quatro alocações. Com ponteiros de 8 bytes e valores int de 4 bytes, o bloco externo usa 24 bytes e as linhas usam 48 bytes, totalizando 72 bytes.

Libere cada linha enquanto seu endereço ainda estiver disponível e depois libere o arranjo de ponteiros. Liberar o bloco externo não libera recursivamente suas linhas. Acessar grid[i] após liberar grid usa armazenamento liberado.

Libere recursos após sucesso parcial

Este exemplo completo acompanha quantas linhas realmente existem:

#include <stdlib.h>

int main(void) {
    size_t rows = 3, made = 0;
    int **grid = malloc(rows * sizeof *grid);
    if (grid == NULL) return EXIT_FAILURE;

    for (; made < rows; ++made) {
        grid[made] = malloc(4 * sizeof *grid[made]);
        if (grid[made] == NULL) break;
    }
    int status = made == rows ? EXIT_SUCCESS : EXIT_FAILURE;
    /* Use the grid here only if every row was allocated. */
    for (size_t i = 0; i < made; ++i) free(grid[i]);
    free(grid);
    return status;
}

Se a segunda linha falhar, made será 1, então somente a linha zero será liberada. Nenhum ponteiro de linha não inicializado é lido. Outra solução útil inicializa ponteiros independentes com NULL e encaminha toda falha para uma única seção de liberação de recursos; free(NULL) torna essa seção segura após sucesso parcial.

Conte os tempos de responsabilidade pela liberação, não as chamadas de alocação. Um buffer criado por malloc e redimensionado com sucesso duas vezes precisa de uma chamada final a free aplicada ao resultado mais recente; uma chamada bem-sucedida a realloc já encerra o tempo de vida do bloco anterior. O alocador pode manter o espaço liberado para alocações posteriores, então um monitor do sistema não precisa mostrar uma queda imediata na memória do processo.

Principais conclusões

  • O responsável libera cada bloco após seu último uso.
  • Somente o endereço inicial de uma alocação ainda válida, ou NULL, é um argumento válido para free.
  • Redefinir um ponteiro não corrige as referências alternativas.
  • Libere os blocos internos antes do bloco externo que contém seus endereços.
  • Caminhos de falha devem liberar todos os blocos obtidos anteriormente.
  • Um redimensionamento bem-sucedido exige uma chamada posterior a free para o bloco mais recente.
3

Redimensione buffers com realloc

A entrada nem sempre chega com um tamanho final conhecido. Um buffer redimensionável permite que um programa reserve mais espaço à medida que descobre quanto precisa. O crescimento seguro preserva os valores existentes e a possibilidade de recuperação em caso de falha. Manter juntos um ponteiro, um comprimento e uma capacidade torna essas regras concretas, enquanto o crescimento geométrico limita a quantidade de cópias repetidas.

3.1 Redimensionamento sem perder o bloco antigo

realloc altera o tamanho de um bloco alocado. Ela pode aumentar ou reduzir o armazenamento, e o endereço retornado pode mudar em ambos os casos. Inclua <stdlib.h> e trate sucesso e falha como eventos diferentes no tempo de vida.

O que permanece e o que muda

Quando um bloco cresce, o conteúdo existente é preservado até o tamanho antigo. Os bytes adicionados não são inicializados. Quando ele diminui, somente a parte inicial que cabe no novo tamanho é preservada. Um arranjo que contém 3, 1, 4 e 1 mantém esses quatro valores quando aumentado para seis valores int; os dois últimos elementos precisam receber valores antes do uso. Com valores int de 4 bytes, isso altera a solicitação de 16 para 24 bytes.

O alocador pode ampliar o bloco no próprio local ou alocar em outro lugar, copiar os bytes preservados e liberar o bloco antigo. Seu código deve funcionar com qualquer um dos resultados.

Receba o resultado em um ponteiro temporário
int *tmp = realloc(nums, 6 * sizeof *nums);
if (tmp == NULL) {
    /* nums still owns its original block. */
} else {
    nums = tmp;
    nums[4] = 5;
    nums[5] = 9;
}

Para essa solicitação de tamanho positivo, uma falha deixa o bloco original alocado e inalterado. O programa pode continuar a usá-lo ou liberá-lo antes de relatar a falha. Em caso de sucesso, a responsabilidade pela liberação passa para o bloco retornado. Atribua o novo resultado antes de acessar o arranjo redimensionado.

A atribuição direta nums = realloc(nums, bytes); perde essa proteção. Se a chamada falhar, ela sobrescreve o único endereço armazenado com NULL. A alocação antiga permanece reservada, mas o programa não pode mais liberá-la.

Ponteiros salvos para elementos devem ser reconstruídos

Suponha que o código salve int *selected = &nums[2];. Um redimensionamento bem-sucedido encerra o tempo de vida do objeto antigo, mesmo quando o endereço retornado por acaso é igual. selected é inválido após esse sucesso. Guarde o índice 2 durante o redimensionamento e depois calcule selected = &nums[2]; a partir do novo ponteiro, se esse índice ainda existir.

Não compare o ponteiro antigo com o resultado para decidir se as referências alternativas precisam ser atualizadas. Use o novo resultado em toda chamada bem-sucedida. Se a redução tiver removido o elemento selecionado, não haverá ponteiro para esse elemento a reconstruir.

Um exemplo completo de crescimento e liberação
#include <stdlib.h>

int main(void) {
    int *nums = malloc(4 * sizeof *nums);
    if (nums == NULL) return EXIT_FAILURE;
    for (size_t i = 0; i < 4; ++i) nums[i] = (int)i;

    int *tmp = realloc(nums, 6 * sizeof *nums);
    if (tmp == NULL) {
        free(nums);
        return EXIT_FAILURE;
    }
    nums = tmp;
    nums[4] = 4;
    nums[5] = 5;
    free(nums);
    return EXIT_SUCCESS;
}

Para uma quantidade variável, verifique a multiplicação por seu tamanho em bytes antes dessa chamada, assim como em malloc. Solicite bytes, não elementos: realloc(nums, 6) solicita seis bytes.

realloc(NULL, positive_size) se comporta como malloc, o que permite que um buffer vazio comece com um ponteiro nulo. Para liberar um buffer, chame free explicitamente. Evite redimensionar para tamanho zero: o comportamento histórico das bibliotecas varia, e redimensionar um bloco existente para zero é comportamento indefinido no C23. Arranjos locais e blocos já liberados não são alvos válidos de redimensionamento.

Principais conclusões

  • Use um ponteiro temporário para que uma falha no redimensionamento para tamanho positivo preserve o endereço do responsável pelo bloco.
  • O crescimento preserva o conteúdo antigo, mas deixa os bytes adicionados não inicializados.
  • A redução preserva apenas a parte que ainda cabe.
  • Após o sucesso, use o ponteiro retornado e reconstrua os ponteiros salvos para elementos.
  • Use free para liberar o armazenamento; trate tamanhos zero explicitamente.

3.2 Um arranjo que cresce com a entrada

Um arranjo que pode crescer precisa de três informações de estado: seu ponteiro, seu comprimento e sua capacidade. O comprimento conta os elementos inicializados em uso. A capacidade conta as posições de elementos alocadas. Mantenha o comprimento menor ou igual à capacidade.

Acrescente em uma ordem segura

Comece com int *data = NULL; size_t len = 0, cap = 0;. Antes de uma inserção, verifique se len == cap. Um arranjo cheio deve crescer antes da escrita em data[len]. Somente após o sucesso do redimensionamento o ponteiro e a capacidade podem mudar. Armazene o novo valor e depois aumente o comprimento.

O exemplo completo a seguir acrescenta dez valores com crescimento geométrico:

#include <stdint.h>
#include <stdlib.h>

int main(void) {
    int *data = NULL;
    size_t len = 0, cap = 0;
    for (int value = 0; value < 10; ++value) {
        if (len == cap) {
            size_t next;
            if (cap == 0) {
                next = 4;
            } else {
                if (cap > SIZE_MAX / 2 / sizeof *data) {
                    free(data);
                    return EXIT_FAILURE;
                }
                next = cap * 2;
            }
            int *tmp = realloc(data, next * sizeof *data);
            if (tmp == NULL) {
                free(data);
                return EXIT_FAILURE;
            }
            data = tmp;
            cap = next;
        }
        data[len++] = value;
    }
    free(data);
    return EXIT_SUCCESS;
}

A proteção contra estouro verifica tanto a duplicação quanto a conversão para bytes antes que qualquer um dos produtos possa sofrer redução modular. A capacidade inicial de quatro também cabe nesse tipo de elemento. Em caso de falha, o programa libera a alocação antiga, pela qual ainda é responsável, e para; ele não atualiza incorretamente a capacidade.

Rastreie comprimento e capacidade separadamente

Começando com capacidade 4, as primeiras quatro inserções usam o espaço existente. Antes da quinta, a capacidade dobra para 8. Antes da nona, dobra para 16. Dez inserções terminam com comprimento 10 e capacidade 16. Se as duas operações de crescimento moverem o bloco, elas copiarão no máximo quatro e oito elementos, totalizando doze.

A capacidade livre é intencional. Crescer um elemento a cada inserção causa cópias repetidas. A partir da capacidade inicial 1, chegar a 1.000 elementos pode exigir 999 redimensionamentos e 499.500 elementos copiados. A duplicação chega à capacidade 1.024 após dez redimensionamentos e no máximo 1.023 cópias. Um redimensionamento específico pode ser custoso, mas o trabalho médio por inserção permanece constante com crescimento geométrico.

Um fator como 1,5 deixa menos espaço livre, mas cresce com mais frequência. Se a entrada tiver terminado, o programa pode reduzir o bloco ao comprimento em uso, tratando a falha com o mesmo padrão de ponteiro temporário. Um arranjo vazio é liberado com free, não com um redimensionamento para tamanho zero.

Buffers de caracteres precisam de uma posição adicional

Para uma cadeia que cresce, reserve espaço para '\0'. Antes de acrescentar um caractere, aumente o buffer se len + 1 >= cap. Com capacidade 8, cabem sete caracteres, deixando a posição do terminador; “hello world” provoca o crescimento para 16 e precisa de doze bytes, incluindo seu terminador.

Um ponteiro salvo para um elemento se torna inválido após um crescimento bem-sucedido. Guarde um índice e acesse novamente o elemento usando o novo endereço inicial. Ao redefinir um arranjo para uso posterior, libere seu bloco e defina data = NULL; len = cap = 0;.

Principais conclusões

  • O comprimento conta os elementos em uso; a capacidade conta as posições disponíveis.
  • Aumente antes de uma inserção quando o comprimento for igual à capacidade.
  • Atualize o ponteiro e a capacidade somente após um redimensionamento bem-sucedido.
  • O crescimento geométrico limita as cópias repetidas ao custo de espaço livre.
  • Proteja os cálculos de crescimento antes da multiplicação.
  • Reserve um byte adicional para o caractere nulo terminador de uma cadeia.
  • Guarde índices durante o crescimento e reconstrua os ponteiros para elementos depois.
4

Reconheça e evite erros de memória

Erros de memória surgem quando regras de responsabilidade pela liberação, tempo de vida, limites ou inicialização são violadas. Cada um deixa um rastro diferente: um endereço perdido, um acesso após a liberação, um índice fora do bloco ou um valor lido cedo demais. Alguns erros encerram o programa imediatamente, enquanto outros corrompem dados e aparecem mais tarde. Ler o código em termos de blocos e seus históricos ajuda a identificar a causa por trás do sintoma.

4.1 Como encontrar o caminho que perde memória

Um vazamento de memória é um armazenamento alocado de que um programa não precisa mais, mas que nunca libera. Muitas vezes, o último ponteiro desaparece, tornando impossível uma chamada posterior a free. Um vazamento pode ocorrer silenciosamente enquanto o uso de memória cresce.

Rastreie endereços, não nomes de variáveis
char *p = malloc(100);
if (p == NULL) return EXIT_FAILURE;
p = malloc(200);  /* loses the only address of the first block */
if (p == NULL) return EXIT_FAILURE;
free(p);

A chamada final a free libera apenas o segundo bloco. O nome p foi reutilizado; a primeira alocação não foi. Se a segunda solicitação falhar, o bloco de 100 bytes ainda estará perdido. Libere o primeiro bloco antes de substituí-lo, quando ele não for mais necessário, ou guarde seu endereço em outra variável responsável por sua liberação.

Atribuir NULL a um ponteiro responsável antes de liberar o bloco causa a mesma perda. A atribuição altera uma variável ponteiro; não informa ao alocador que deve liberar algo. Variáveis locais automáticas desaparecem ao final de uma chamada, mas os blocos alocados para os quais apontavam permanecem.

Examine todas as saídas de uma função

Um retorno antecipado pode ignorar a liberação de recursos:

char *line = malloc(100);
if (line == NULL) return EXIT_FAILURE;
if (input_is_bad) {
    free(line);
    return EXIT_FAILURE;
}
/* Use line on the successful path. */
free(line);

Ambos os caminhos liberam o bloco. Em uma função com mais recursos, uma única seção de liberação reduz a quantidade de caminhos a examinar. Uma função que retorna um resultado alocado transfere a responsabilidade por sua liberação; uma função que apenas abandona um ponteiro local responsável causa um vazamento.

A atribuição direta do resultado de uma chamada malsucedida a realloc é outro caminho de perda. Alocações aninhadas vazam quando seu arranjo externo de ponteiros é liberado antes que os endereços internos sejam usados para liberar os recursos.

A repetição torna pequenos vazamentos custosos

Suponha que um laço aloque 100 bytes por passagem e nunca os libere. Após 1.000 passagens, ele terá abandonado 100.000 bytes. Libere cada buffer ao final de sua passagem ou aloque uma vez antes do laço, reutilize o bloco e libere-o depois. A reutilização também evita trabalho repetido do alocador quando a mesma capacidade é suficiente.

Um serviço que deixa vazar 64 bytes por solicitação perde 640.000 bytes após 10.000 solicitações, exatamente 625 KiB. Após um milhão de solicitações, terá perdido 64.000.000 bytes, cerca de 61 MiB. Programas de longa duração podem acabar falhando ao alocar memória ou deixando o sistema lento. O sistema operacional normalmente recupera a memória do processo ao término, mas isso não ajuda o serviço enquanto ele executa.

Perdida e ainda acessível são relatórios diferentes

Se um ponteiro global ainda contiver o endereço inicial de uma alocação ao término, o Valgrind classificará o bloco como “ainda acessível” (still reachable). O endereço não foi perdido; o programa simplesmente não liberou o bloco. “Definitivamente perdida” (definitely lost) significa que não resta nenhum ponteiro utilizável. Planeje a liberação de recursos para que esses relatórios mostrem claramente as alocações sem explicação.

Principais conclusões

  • Sobrescrever ou zerar o último ponteiro responsável pode causar o vazamento de um bloco.
  • Os caminhos normais, de retorno antecipado e de falha precisam liberar recursos.
  • Pequenos vazamentos repetidos se acumulam durante a execução do programa.
  • Reutilizar um buffer adequado pode evitar tanto o trabalho de alocação repetido quanto vazamentos por iteração.

4.2 Quando um ponteiro sobrevive ao objeto para o qual aponta

Um ponteiro se torna pendente quando termina o tempo de vida do objeto para o qual ele aponta. A variável ainda pode existir, mas ler ou escrever o objeto antigo por meio desse ponteiro é inválido. Retornar um endereço local e liberar um bloco alocado podem criar esse problema.

Encontre o último uso antes da liberação
int *scores = malloc(3 * sizeof *scores);
if (scores == NULL) return EXIT_FAILURE;
scores[0] = 90;
scores[1] = 75;
scores[2] = 82;
free(scores);
printf("%d\n", scores[0]);  /* use-after-free */

A atribuição a scores[0] ocorreu durante o tempo de vida do bloco. A leitura final não. Mova free(scores) para depois da impressão, quando todos os usos estiverem concluídos. Apenas atribuir scores = NULL antes da impressão transforma o acesso inválido em uma desreferenciação de ponteiro nulo; isso não fornece os dados ausentes.

Isso é comportamento indefinido: C não impõe requisitos sobre o resultado. O programa pode exibir um valor antigo, encerrar inesperadamente ou danificar dados silenciosamente. Os bytes liberados frequentemente permanecem no local até que o alocador os reutilize. Portanto, um resultado plausível não valida uma leitura após a liberação.

Referências alternativas compartilham o problema de tempo de vida
int *a = malloc(3 * sizeof *a);
if (a == NULL) return EXIT_FAILURE;
int *b = a;
free(a);
a = NULL;
b[0] = 1;  /* b also lost its live target */

Havia um bloco, com duas variáveis ponteiro contendo seu endereço. Liberá-lo por meio de a encerra seu tempo de vida para ambos os ponteiros. Atribuir nulo a a altera apenas a; isso não pode atualizar b. O código que usa referências alternativas emprestadas deve terminar antes que o responsável libere o bloco, e essas referências não devem ser usadas depois.

A liberação dupla é outro uso após o fim do tempo de vida

Chamar free duas vezes para um bloco é uma liberação dupla, incluindo uma segunda chamada por meio de uma referência alternativa. Isso pode corromper os registros de controle do alocador ou fazer uma alocação posterior falhar. Algumas bibliotecas detectam o problema imediatamente e interrompem o programa, mas essa detecção imediata não é garantida.

Atribuir NULL à variável responsável após a liberação ajuda em uma chamada repetida de liberação, pois free(NULL) é válida. Isso não torna free(b) segura quando b ainda se refere ao bloco anterior. Exatamente um responsável deve decidir quando o bloco será liberado.

O redimensionamento também encerra o tempo de vida antigo

Uma chamada bem-sucedida a realloc substitui o objeto antigo mesmo que o endereço numérico retornado não mude. Ponteiros salvos para o interior do bloco antigo devem ser recalculados a partir do ponteiro retornado. Armazene posições como índices enquanto o crescimento puder ocorrer. Uma chamada malsucedida a realloc com tamanho positivo, por outro lado, mantém a alocação antiga válida.

Uma função que retorna o endereço de uma variável local automática cria um erro de tempo de vida semelhante sem chamar free: o objeto deixa de existir quando a função retorna. Use armazenamento de responsabilidade da função chamadora ou um resultado no heap com responsável definido. O uso após a liberação e a liberação dupla também podem se tornar defeitos de segurança quando um atacante pode influenciar o armazenamento reutilizado.

Principais conclusões

  • Um ponteiro pendente aponta para um objeto cujo tempo de vida terminou.
  • Conclua todas as leituras e escritas antes de liberar o bloco.
  • Uma liberação invalida todas as referências alternativas ao objeto antigo.
  • Atribuir nulo ao ponteiro responsável ajuda em liberações repetidas, mas não corrige referências alternativas emprestadas.
  • Uma liberação dupla pode danificar os registros de controle do alocador.
  • Uma chamada bem-sucedida a realloc invalida ponteiros salvos para o interior do bloco antigo.

4.3 Limites válidos e valores inicializados

Um acesso válido a um elemento exige armazenamento suficiente e um valor atribuído antes da leitura. Um bloco pode ter o tamanho correto, mas conter dados não inicializados, ou conter valores úteis, mas ser acessado fora de seus limites.

Verifique a faixa de elementos

Para n elementos, os índices válidos vão de zero a n - 1. Um laço com i <= n realiza um acesso a mais. Com dez valores int de 4 bytes, a alocação é de 40 bytes; p[10] começa imediatamente após esses bytes. Use i < n para percorrer os dez elementos.

Um índice obtido da entrada também precisa de uma verificação de faixa antes do uso. Uma entrada com sinal deve ser não negativa e menor que a quantidade de elementos. Mantenha a quantidade junto ao ponteiro e passe ambos às funções. Um ponteiro sozinho não descreve a faixa permitida.

Dimensione cadeias de caracteres para seu terminador
char *copy = malloc(strlen(s));
strcpy(copy, s);  /* writes the terminator beyond the allocation */

Esse trecho incorreto também precisa de uma verificação de falha. A correção do tamanho é malloc(strlen(s) + 1), seguida da verificação e depois da cópia. “Amsterdam” tem nove caracteres, mas precisa de dez bytes com '\0'. O terminador é um byte realmente armazenado, embora não seja impresso como parte do texto.

Um arranjo de ponteiros não armazena as cadeias de caracteres
char **names = malloc(n * sizeof *names);

Isso reserva n posições para ponteiros. Não cria buffers de caracteres para esses ponteiros endereçarem. Antes de usar names[i] como uma cadeia de caracteres, atribua a ele um endereço válido de cadeia. Ele pode tomar emprestado um arranjo existente ou um literal com tempo de vida suficiente, ou ser responsável por uma cópia da cadeia alocada separadamente. Apenas as cadeias sob sua responsabilidade são liberadas durante a liberação dos blocos aninhados.

O mesmo raciocínio de tamanho se aplica a arranjos numéricos: malloc(n) conta bytes, e sizeof p mede um ponteiro. Use n * sizeof *p, com a proteção contra estouro e a verificação de ponteiro nulo, para expressar corretamente uma quantidade de elementos.

Escreva antes de ler
int *t = malloc(3 * sizeof *t);
if (t == NULL) return EXIT_FAILURE;
t[0] = 5;
t[1] = 7;
printf("%d\n", t[0] + t[1] + t[2]);  /* t[2] was never set */
free(t);

t[2] está dentro do bloco alocado, mas seu valor não foi inicializado. Atribua um valor a ele antes da soma ou use calloc quando zero for o valor inicial desejado. Os bytes adicionados durante o crescimento com realloc precisam da mesma atenção. Estar dentro dos limites não inicializa a memória.

Por que o encerramento inesperado pode ocorrer depois

Uma escrita fora dos limites pode danificar dados próximos ou os registros de controle do alocador. Uma leitura fora dos limites pode expor dados não relacionados. Ambas causam comportamento indefinido. Um pequeno acesso além dos limites pode atingir memória mapeada e escapar à detecção imediata; uma chamada posterior a free pode ser a primeira falha visível.

Verifique independentemente o tamanho da alocação, a faixa de índices e o histórico de inicialização. Uma alocação maior não pode fornecer um valor significativo a um elemento não inicializado, e preencher com zeros não pode tornar válido um índice fora da faixa.

Principais conclusões

  • Os índices válidos para n elementos vão de zero a n - 1.
  • O armazenamento de uma cadeia de caracteres inclui um byte para o caractere nulo terminador.
  • Um arranjo de ponteiros para cadeias de caracteres não aloca as próprias cadeias.
  • Inicialize todos os valores lidos por um cálculo, incluindo os elementos adicionados durante o crescimento.
  • Uma escrita inválida pode corromper dados muito antes de um encerramento inesperado visível.

4.4 Rastreamento de um encerramento inesperado até sua causa

Uma falha de segmentação é uma resposta do sistema operacional a um acesso proibido à memória. Causas comuns incluem desreferenciar um ponteiro nulo ou não inicializado, escrever em um literal de cadeia de caracteres, acessar muito além dos limites de um arranjo, usar armazenamento liberado e esgotar a pilha.

O que a mensagem informa

No Linux, SIGSEGV geralmente interrompe o processo, e um shell pode imprimir “Segmentation fault (core dumped)”. Um código de saída comum do shell é 139, pela convenção de sinais de 128 mais o sinal 11. O macOS pode relatar SIGSEGV ou SIGBUS para acessos inválidos. O Windows chama a falha equivalente de violação de acesso, frequentemente com o código de exceção 0xC0000005.

Essas mensagens identificam um acesso malsucedido, não seu histórico completo. Um pequeno acesso além dos limites pode corromper um ponteiro no início e causar um encerramento inesperado muito depois. Muitos acessos inválidos permanecem dentro da memória que o processo pode endereçar, então corrompem dados silenciosamente em vez de gerar uma falha.

Comece com informações de linha

Compile um executável para depuração com avisos:

cc -std=c17 -g -O0 -Wall -Wextra program.c -o program

Depois, execute-o em um depurador ou verificador de memória. Um rastreamento da pilha de chamadas lista as chamadas de função ativas no momento da falha. Encontre o registro de ativação mais acima que pertença ao seu próprio código. Funções de biblioteca como free podem ser o lugar onde um estado incorreto se torna visível, enquanto uma chamada anterior em seu programa o criou.

Na linha relatada, identifique cada ponteiro desreferenciado. Para cada um, rastreie seu valor até a origem. A alocação foi verificada? O objeto para o qual ele aponta ainda existe? O índice cabe no bloco? O ponteiro chegou a receber um endereço válido?

Analise um resultado nulo
int *p = NULL;
printf("%d\n", *p);  /* invalid dereference */

A impressão exige uma leitura por meio de p, mas não existe um objeto inteiro nesse endereço. Se p veio de uma função de alocação, verifique o resultado antes dessa linha e siga um caminho de erro em caso de falha. Se recebeu nulo após a liberação, mova o uso necessário para antes da liberação ou repense onde os dados são armazenados.

Um ponteiro não inicializado, como em int *p; *p = 5;, não recebeu um objeto válido para o qual apontar. Uma alocação, o endereço de um objeto ainda válido ou um ponteiro emprestado verificado deve estabelecer esse objeto antes da desreferenciação.

Acompanhe a causa quando os sintomas mudarem

Uma função recursiva infinita pode esgotar os registros de ativação da pilha em uma versão sem otimização. Uma condição de parada corrige a lógica. Um compilador com otimização pode transformar algumas chamadas recursivas em um laço, então a ausência de um encerramento por estouro de pilha não torna útil o cálculo sem limite.

Mensagens impressas de progresso também são evidências imperfeitas. stdout é armazenada em buffer: a saída pode não ser gravada quando um processo encerra inesperadamente, especialmente com saída redirecionada. Para uma mensagem de diagnóstico, use stderr ou chame fflush(stdout) após uma impressão de progresso. Acrescentar uma impressão pode alterar a disposição da memória e esconder um erro; isso não corrigiu o acesso inválido. Execute novamente com um verificador para encontrar a própria operação com defeito.

Principais conclusões

  • Uma falha de segmentação indica um acesso proibido; a corrupção original pode ter ocorrido antes.
  • Compile com informações de depuração e localize o primeiro registro relevante em seu próprio código.
  • Rastreie a origem de cada ponteiro, o tempo de vida do objeto para o qual aponta e a faixa de índices.
  • A saída armazenada em buffer pode ocultar uma impressão executada antes de um encerramento inesperado.
  • Uma mudança no sintoma após acrescentar uma impressão ainda precisa ser investigada.
5

Encontre erros de memória com Valgrind e sanitizadores

Verificadores de memória tornam erros ocultos visíveis ao relatar o acesso e o histórico do bloco. O Valgrind observa um executável para depuração; o AddressSanitizer acrescenta verificações durante a compilação. Aprenda a relacionar suas mensagens aos limites dos elementos, aos locais de alocação e aos locais de liberação e, depois, faça uma correção que preserve a responsabilidade adequada pela liberação. Após uma execução sem erros, use novas entradas que exercitem outros caminhos.

5.1 Leitura de um relatório do Valgrind

A ferramenta padrão do Valgrind, o Memcheck, observa um programa compilado durante sua execução. Ela acompanha os limites do heap, os tempos de vida das alocações e se os valores foram inicializados. Não precisa de uma compilação com sanitizador, mas as informações de depuração tornam seus relatórios muito mais fáceis de usar.

Compile e execute
cc -std=c17 -g -O0 -Wall -Wextra program.c -o program
valgrind --leak-check=full ./program

Coloque os argumentos do próprio programa após ./program. Acrescente --show-leak-kinds=all para listar todas as categorias de vazamento e --track-origins=yes quando a origem de um valor não inicializado não estiver clara.

O Valgrind executa no Linux e em alguns outros sistemas semelhantes ao Unix, mas não nativamente no Windows ou em Macs com Apple silicon. O Memcheck frequentemente torna a execução aproximadamente 20 a 30 vezes mais lenta. Use primeiro uma pequena entrada que reproduza o problema e depois execute outros testes. Um teste sem erros cobre apenas os caminhos que realmente executa.

Leia o tamanho do acesso e o tamanho do bloco separadamente

Um relatório com o cabeçalho “Invalid write of size 4” descreve uma escrita de quatro bytes. Um endereço descrito como “0 bytes after a block of size 40 alloc'd” começa imediatamente após uma alocação de 40 bytes. Se esse bloco contiver dez valores int de 4 bytes, o acesso inválido começa no elemento 10; o último elemento válido é o 9.

O relatório também indica a linha do acesso e a pilha de chamadas da alocação. Use-os em conjunto: a alocação informa os limites pretendidos, e o acesso informa qual índice os violou. “Size 4” não significa que o programa ultrapassou o limite em quatro elementos.

“Invalid read of size N” descreve, da mesma forma, uma leitura inválida. Um endereço dentro de um bloco descrito como “free'd” indica um problema de tempo de vida; compare os locais de alocação, liberação e acesso. “Invalid free() / delete / delete[] / realloc()” pode indicar uma liberação duplicada ou um endereço que nunca foi o início de uma alocação válida.

Para uma condição que usa dados sem valor atribuído, o Memcheck pode informar “Conditional jump or move depends on uninitialised value(s)”. Essa grafia pertence à mensagem da ferramenta. A condição relatada consumiu o valor; --track-origins=yes ajuda a localizar a origem do armazenamento não inicializado.

Leia a categoria do vazamento
Categoria do relatórioSignificado
Definitivamente perdidaNão resta nenhum ponteiro para o bloco
Indiretamente perdidaO bloco só é acessível por meio de outro bloco perdido
Possivelmente perdidaApenas um ponteiro para o interior foi encontrado
Ainda acessívelUm ponteiro permanece ao término, mas o bloco não foi liberado

Suponha que uma função auxiliar aloque dez valores int de 4 bytes em um ponteiro local e retorne sem liberar ou retornar o endereço. Seu bloco de 40 bytes está definitivamente perdido. Um ponteiro global que mantivesse o endereço inicial faria com que um bloco não liberado fosse classificado como ainda acessível. Os totais de alocações e liberações do resumo do heap descrevem eventos, incluindo redimensionamentos; eles não substituem o raciocínio sobre responsabilidade pela liberação.

Um término sem erros pode apresentar “All heap blocks were freed -- no leaks are possible” e “ERROR SUMMARY: 0 errors from 0 contexts”. O Memcheck geralmente não detecta todo acesso além dos limites de arranjos locais ou globais que permaneça em memória válida. Corrija o primeiro erro relatado, execute novamente e teste outras entradas, incluindo entrada vazia e caminhos com retorno antecipado.

Principais conclusões

  • Use informações de depuração para que o Memcheck possa identificar suas linhas de código-fonte.
  • Leia em conjunto o tamanho do acesso, os limites do bloco e o histórico de alocação.
  • Use --track-origins=yes para rastrear valores não inicializados.
  • Diferencie blocos perdidos de blocos ainda acessíveis ao término.
  • Corrija o primeiro erro antes de interpretar as consequências posteriores.
  • Execute com várias entradas, pois um verificador observa apenas os caminhos executados.

5.2 Inclusão de verificações no programa

O AddressSanitizer, ou ASan, acrescenta verificações de acesso à memória durante a compilação. O executável resultante verifica a si próprio durante a execução. Ele é útil para testes rotineiros de desenvolvimento, pois normalmente torna a execução cerca de duas vezes mais lenta, muito menos que o Memcheck.

Compile e faça a ligação com o sanitizador
cc -std=c17 -g -fsanitize=address -fno-omit-frame-pointer \
    program.c -o program-asan
./program-asan

Use a opção do sanitizador tanto na compilação quanto na ligação final ao executar essas etapas separadamente. -g fornece informações de linha, e -fno-omit-frame-pointer ajuda a preservar pilhas de chamadas úteis. O compilador da Microsoft usa /fsanitize=address.

O ASan detecta acessos além dos limites de arranjos no heap, na pilha e globais, uso após a liberação, liberações inválidas e liberações duplas. Verificações do tempo de vida no escopo e verificações opcionais de uso após o retorno também podem detectar ponteiros para antigas variáveis locais. Ele exige recompilação e memória adicional. Mantenha essa instrumentação nas versões de desenvolvimento e teste.

Leia os três históricos

Um relatório de uso após a liberação pode começar com “ERROR: AddressSanitizer: heap-use-after-free”. A mensagem seguinte, “READ of size 4”, descreve a operação inválida. O relatório mostra então onde a memória foi “freed by thread T0 here:” e “previously allocated by thread T0 here:”.

Comece pela primeira linha relevante em seu código em cada pilha de chamadas. A alocação estabelece o bloco; a liberação encerra seu tempo de vida; a leitura relatada ocorre depois desse encerramento. Decida se deve mover a liberação para depois da leitura ou parar de acessar o bloco após a liberação. O relatório fornece o histórico, enquanto a responsabilidade pretendida pela liberação determina a correção.

Para “heap-buffer-overflow”, um endereço descrito como “located 0 bytes after 20-byte region” começa imediatamente após a região. Converta bytes em elementos somente depois de verificar o tamanho do tipo usado pelo programa. O ASan normalmente para no primeiro erro, então corrigi-lo pode revelar outro erro na próxima execução.

Verificações de vazamento e valores não inicializados são diferentes

O LeakSanitizer verifica alocações não liberadas ao término. Ele normalmente é ativado com o ASan no Linux. Em cadeias de ferramentas compatíveis do macOS, pode ser ativado com ASAN_OPTIONS=detect_leaks=1; algumas cadeias de ferramentas para Mac não o oferecem. A ferramenta separada leaks da Apple é outra opção de verificação de vazamentos nesses casos.

O ASan não acompanha se um valor dentro dos limites foi inicializado. Para esse problema, use o Memcheck ou o MemorySanitizer do Clang em uma configuração Linux compatível. O UndefinedBehaviorSanitizer verifica outros tipos de comportamento indefinido, como estouro de inteiro com sinal, e pode ser combinado com o ASan usando -fsanitize=address,undefined.

Escolha uma execução separada para cada verificador
NecessidadeFerramenta útil
Verificações rápidas durante o desenvolvimento, incluindo limites de arranjos locaisVersão compilada com ASan
Rastreamento de valores não inicializados sem instrumentação de sanitizadorMemcheck em um sistema compatível
Vazamentos de alocação ao términoLeakSanitizer ou Memcheck

Não execute um executável com ASan no Valgrind. Compile um executável comum separado para depuração com o Memcheck e não combine ASan e MemorySanitizer em uma mesma compilação. Após uma execução sem erros, varie a entrada para que outros caminhos de alocação, crescimento e liberação sejam executados.

Principais conclusões

  • O ASan exige compilação e ligação com sua opção de instrumentação.
  • Leia as pilhas do acesso inválido, da liberação e da alocação para reconstruir um erro de tempo de vida.
  • O LeakSanitizer verifica vazamentos ao término onde a plataforma oferece suporte.
  • O ASan não detecta leituras comuns de valores não inicializados.
  • Use execuções separadas e versões compiladas adequadas para ASan e Memcheck.

5.3 Transformação de um relatório em uma correção verificada

Um relatório de memória identifica um erro observado e partes de seu histórico. Uma correção útil restaura o tamanho, os limites, a inicialização e a responsabilidade pela liberação corretos e depois verifica novamente o programa corrigido.

Reduza e reproduza

Encontre a menor entrada que ainda produza o erro. Compile com -g, avisos e pouca otimização e depois use o Memcheck ou uma versão com ASan. Leia o primeiro relatório antes de investigar os posteriores; um estado corrompido pode gerar uma cadeia de falhas enganosas.

Suponha que um programa contenha:

size_t n = 5;
int *v = malloc(n * sizeof *v);
for (size_t i = 0; i <= n; ++i) v[i] = (int)(i * i);
printf("%d\n", v[2]);
return 0;

Em um sistema com valores int de 4 bytes, o ASan relata uma escrita de quatro bytes no primeiro byte após uma alocação de 20 bytes. A escrita está no laço. O Memcheck também pode relatar o acesso além dos limites e, depois, uma alocação não liberada; se o bloco está perdido ou ainda acessível depende de quais ponteiros permanecem ao término.

Relacione o relatório ao código

Cinco valores int ocupam os índices de zero a quatro. A condição i <= n alcança o índice cinco. O tamanho da alocação está correto; o limite do laço não está. Aumentar o bloco esconderia o sintoma, mas deixaria o percurso incorreto.

Também não há verificação da alocação, e o bloco sob responsabilidade do programa não é liberado. Esses problemas exigem suas próprias correções, mesmo que o primeiro relatório tenha interrompido o programa antes de alcançá-los.

#include <stdio.h>
#include <stdlib.h>

int main(void) {
    size_t n = 5;
    int *v = malloc(n * sizeof *v);
    if (v == NULL) return EXIT_FAILURE;
    for (size_t i = 0; i < n; ++i) v[i] = (int)(i * i);
    printf("%d\n", v[2]);
    free(v);
    return EXIT_SUCCESS;
}

O programa corrigido inicializa cinco elementos válidos, imprime 4 enquanto o bloco ainda existe e o libera. A pequena quantidade fixa cabe no tipo; uma quantidade obtida da entrada também precisa da verificação de estouro no cálculo do tamanho antes da alocação.

Use o histórico do relatório para fazer a correção adequada

Para uso após a liberação, compare as pilhas de liberação e acesso para encontrar o último uso legítimo. Mova a liberação para depois dele ou remova o acesso inválido posterior. Atribuir NULL a um ponteiro não pode tornar seus dados anteriores disponíveis novamente.

Para um vazamento, a pilha de alocação informa onde o bloco começou. Ela não identifica a linha correta para a liberação. Rastreie a responsabilidade pela liberação nos retornos bem-sucedidos e nas falhas para encontrar onde termina o último uso. Para uma liberação dupla, remova a liberação duplicada ou corrija os responsáveis conflitantes. Remover a única liberação válida cria um vazamento.

Para dados não inicializados, acrescente --track-origins=yes ao Memcheck. A linha que consome os dados pode estar longe da alocação que forneceu bytes sem valores definidos. Inicialize cada valor lido pelo cálculo ou use calloc quando zero for o estado inicial correto.

Verifique a correção em mais de um caminho

Recompile e execute novamente a entrada original que reproduzia o problema. Continue até que seu relatório não apresente erros e depois tente entrada vazia, limites de crescimento, entrada comum e retornos por erro. Os avisos do compilador detectam alguns problemas antes da execução; ferramentas de execução verificam os caminhos alcançados por esses testes.

Principais conclusões

  • Reproduza com uma entrada pequena e leia o primeiro erro.
  • Use o tamanho do bloco e a posição do acesso para identificar o erro real de limite.
  • Verifique a falha de alocação e a liberação de recursos mesmo quando o primeiro erro de acesso interromper a execução.
  • Relatórios de vazamento mostram locais de alocação; a responsabilidade pela liberação determina os locais de liberação.
  • Recompile, execute novamente o caso de reprodução e exercite outros caminhos após a correção.

Exercícios resolvidos de Gerenciamento de memória dinâmica em C

5 Lições Interativas

1

Acompanhe um objeto ao longo de chamadas de função

9 passos
Rastreie dados locais, estáticos e alocados e depois escolha o armazenamento para um resultado retornado.
  1. 1
    IntroduçãoUm endereço precisa de um objeto ainda válido

    Um ponteiro permite que uma parte de um programa encontre dados criados em outra parte. Sua utilidade depende de esses dados ainda existirem quando o ponteiro for usado.

    Rastreie uma função auxiliar que cria um arranjo de inteiros, retorna seu endereço e permite que a função chamadora use o resultado. Mantenha o ponteiro local e o arranjo separados ao acompanhar a chamada: eles não precisam ter o mesmo tempo de vida.

  2. 2
    ExplicaçãoNomeie os três tempos de vida

    Uma variável local comum tem duração de armazenamento automática e deixa de existir com seu bloco. Uma variável global ou local static dura toda a execução do programa. Um bloco alocado dura até a desalocação.

    Nos sistemas mais usados, objetos automáticos ocupam a pilha de chamadas e alocações usam o heap. Esses nomes descrevem implementações comuns; as regras de C definem os tempos de vida independentemente da disposição da memória.

  3. 3
    ExemploCrie o ponteiro local e o bloco

    Dentro de make_array, int *p = malloc(5 * sizeof *p); cria dois objetos separados: o ponteiro local e, em caso de sucesso, uma alocação para cinco valores int.

    Quando um int ocupa 4 bytes, o bloco tem 20 bytes. Verifique p antes do uso. A variável ponteiro pertence à chamada de função, mas o bloco alocado não faz parte do armazenamento automático dessa chamada.

  4. 4
    ExemploRetorne o endereço

    A função auxiliar retorna p, e sua função chamadora armazena o resultado em int *nums. O endereço é copiado; o arranjo não é. As variáveis locais da função auxiliar então deixam de existir.

    Em caso de sucesso, nums ainda identifica o arranjo alocado. A função chamadora verifica o resultado, inicializa e usa seus elementos e, por fim, libera o bloco. O retorno não liberou o bloco porque sua duração de armazenamento é alocada.

  5. 5
    DesafioO que sobrevive ao retorno?

    Uma função auxiliar cria uma variável automática int count e um arranjo separado no heap. Ela retorna o endereço do arranjo. Qual desses dois objetos a função chamadora ainda pode usar?

    ---reveal---

    O arranjo no heap permanece válido até ser liberado. A variável automática count da função auxiliar deixou de existir no retorno. O endereço copiado alcança apenas o arranjo; guardar um endereço não preservaria count se a função auxiliar tivesse retornado &count.

  6. 6
    ExemploTente retornar um arranjo local

    Agora altere a função auxiliar para usar int values[5]; e retornar values. Esse arranjo é automático. Ele deixa de existir quando a função auxiliar retorna, deixando a função chamadora sem um objeto válido para acessar.

    O tamanho ainda é de cinco valores int, mas o tempo de vida é diferente. Um resultado utilizável pode ser colocado em um buffer fornecido pela função chamadora ou alocado e transferido a ela com uma responsabilidade clara pela liberação.

  7. 7
    ConexãoEscolha um contrato para o buffer

    Um buffer estático de resultado sobrevive ao retorno, mas todas as chamadas o compartilham. Uma chamada posterior pode sobrescrever um resultado anterior. Um resultado no heap pode dar a cada chamada seu próprio armazenamento independente, com a função chamadora responsável pela liberação.

    Para trabalho temporário, um pequeno arranjo local fixo é mais simples. Arranjos com tamanho definido pela entrada, muito grandes ou que podem crescer exigem alocação verificada, para que o programa possa tratar falhas e controlar o tempo de vida.

  8. 8
    DesafioDois resultados devem permanecer independentes

    Uma função chamadora precisa de dois resultados graváveis de chamadas sucessivas e deve manter ambos. Ela não fornece um buffer. Um único arranjo estático de resultado atenderia a essa necessidade? Qual solução é adequada?

    ---reveal---

    Um único arranjo estático seria compartilhado e sobrescrito. Retorne um resultado alocado separadamente em cada chamada, verifique cada resultado e deixe que a função chamadora libere ambos após seus últimos usos. O tempo de vida no heap garante que os resultados permaneçam; blocos separados garantem a independência.

  9. 9
    RecapitulaçãoSepare o endereço do tempo de vida

    Rastreie o objeto para o qual um ponteiro aponta: variáveis locais automáticas deixam de existir com seu bloco, objetos estáticos duram toda a execução e blocos alocados precisam de liberação deliberada.

    Para um resultado retornado, pergunte se o armazenamento permanece válido e se as funções chamadoras precisam de cópias independentes. Para trabalho temporário, pergunte se um pequeno arranjo local fixo já atende à necessidade. O tamanho e o tempo de vida necessários determinam a solução antes de escrever qualquer chamada de alocação.

2

Aloque uma grade e libere recursos em cada caminho

13 passos
Parta dos tamanhos dos elementos para solicitações verificadas, valores inicializados e liberação de recursos após sucesso parcial.

Salve esta aula para seguir cada Lição Interativa passo a passo.

  1. 1
    IntroduçãoPlaneje tanto o sucesso quanto a falha
  2. 2
    ExplicaçãoTransforme quantidades em bytes
  3. 3
    DicaProteja antes de multiplicar
  4. 4
    DesafioTamanho do elemento ou do ponteiro?
  5. 5
    ExplicaçãoDê um valor inicial aos contadores
  6. 6
    ExemploUse a linha preenchida com zeros
  7. 7
    DesafioE quanto ao crescimento posterior?
  8. 8
    ExplicaçãoConte blocos, não nomes de ponteiros
  9. 9
    ExemploInterrompa a construção em caso de falha parcial
  10. 10
    DesafioEscolha a ordem de liberação
  11. 11
    Ideia-chaveUm responsável pode emprestar ponteiros
  12. 12
    DesafioUm caminho para antecipadamente
  13. 13
    RecapitulaçãoPlaneje a liberação junto com a construção
3

Acrescente valores sem perder dados

12 passos
Rastreie o comprimento, a capacidade e as falhas de redimensionamento de um arranjo dinâmico, incluindo o caso do terminador de cadeia de caracteres.

Salve esta aula para seguir cada Lição Interativa passo a passo.

  1. 1
    IntroduçãoAumente antes da próxima gravação
  2. 2
    ExplicaçãoO redimensionamento pode substituir o objeto
  3. 3
    ExemploSepare falha de sucesso
  4. 4
    DesafioO que pode ser preservado após a redução?
  5. 5
    ExplicaçãoComprimento e capacidade respondem a perguntas diferentes
  6. 6
    ExemploUltrapasse o primeiro limite de crescimento
  7. 7
    ExemploUltrapasse o próximo limite
  8. 8
    Ideia-chaveO espaço livre reduz a quantidade de cópias
  9. 9
    DesafioQual é o estado após uma inserção malsucedida?
  10. 10
    ConexãoUma cadeia de caracteres reserva uma posição para o terminador
  11. 11
    DesafioUma seleção emprestada passa por um crescimento
  12. 12
    RecapitulaçãoAtualize o estado somente após o sucesso
4

Examine todo o histórico de um bloco

15 passos
Diferencie vazamentos, referências alternativas inválidas, erros de limite e valores não inicializados e depois rastreie um encerramento inesperado até sua causa.

Salve esta aula para seguir cada Lição Interativa passo a passo.

  1. 1
    IntroduçãoQuatro perguntas para cada acesso
  2. 2
    ExemploPerca uma alocação ao substituir seu endereço
  3. 3
    DesafioAtribuir nulo ao ponteiro libera a memória?
  4. 4
    DicaExamine as saídas e também os laços
  5. 5
    ExemploLibere uma vez e invalide todas as referências alternativas
  6. 6
    DesafioCorrija o último uso
  7. 7
    Ideia-chaveA saída esperada pode vir de código inválido
  8. 8
    ExemploConte o terminador nulo
  9. 9
    ExemploAlocado não significa inicializado
  10. 10
    DesafioIdentifique dois limites separados
  11. 11
    ExplicaçãoUma falha é um sintoma
  12. 12
    ExemploLeia o acesso que causou o encerramento
  13. 13
    DesafioUma impressão faz o encerramento desaparecer
  14. 14
    DesafioCombine responsabilidade pela liberação e limites
  15. 15
    RecapitulaçãoIdentifique a regra violada
5

Leia um relatório e verifique a correção

14 passos
Escolha um verificador, interprete bytes e pilhas de chamadas e corrija um acesso fora dos limites e uma liberação de recursos ausente.

Salve esta aula para seguir cada Lição Interativa passo a passo.

  1. 1
    IntroduçãoTransforme uma mensagem no histórico de um bloco
  2. 2
    ExplicaçãoUse uma versão comum para depuração com o Memcheck
  3. 3
    ExemploInterprete a descrição do endereço
  4. 4
    DesafioLeia bytes como bytes
  5. 5
    DicaEscolha uma opção de relatório para o problema
  6. 6
    ExplicaçãoCompile um executável com ASan separadamente
  7. 7
    ExemploAcompanhe o uso após a liberação por três pilhas
  8. 8
    DesafioUma execução sem erros no ASan deixa passar um erro de valor
  9. 9
    ExemploRelacione o relatório ao laço
  10. 10
    ExemploMantenha o bloco válido durante o trabalho
  11. 11
    DesafioOnde um vazamento deve ser corrigido?
  12. 12
    ConexãoVerifique o término e o suporte da plataforma
  13. 13
    DesafioConclua o ciclo de correção
  14. 14
    RecapitulaçãoUse evidências, corrija e execute novamente

Questões de Gerenciamento de memória dinâmica em C

50 Questões de Prática em 5 conjuntos

Conjunto 1: Tempos de vida, contagens de bytes e primeiras verificações10 Questões de Prática

Questão 1 de 10

Após uma chamada bem-sucedida int *hits = calloc(6, sizeof *hits);, qual é o valor de hits[4] antes de qualquer atribuição posterior?
  1. Um valor não inicializado
  2. O inteiro 6
  3. 0
  4. O inteiro 4
Mostrar respostaOcultar resposta
Resposta: C
calloc define cada byte como zero, o que dá aos elementos inteiros o valor zero. O índice 4 está dentro do arranjo de seis elementos, então pode ser lido como zero após a verificação da alocação. Um armazenamento recém-alocado por malloc não seria inicializado; essa regra não se aplica a esta alocação bem-sucedida por calloc.

Questão 2 de 10

Um buffer contém seis valores int inicializados. O código salva o índice 4, reduz o bloco com sucesso para três valores int e depois quer o elemento selecionado anteriormente. Qual é uma próxima ação válida?
  1. Reconstruir o ponteiro selecionado como &data[4].
  2. Rejeitar a seleção porque o índice 4 foi removido.
  3. Ler pelo ponteiro selecionado antigo antes de outra alocação.
  4. Ler o índice 4 se o endereço redimensionado não mudou.
Mostrar respostaOcultar resposta
Resposta: B
A redução preserva apenas os três primeiros elementos, nos índices de zero a dois. O índice 4 não identifica mais um elemento no bloco redimensionado, então ele não pode ser recuperado recalculando um ponteiro. O tempo de vida do objeto antigo também terminou com o sucesso, mesmo que o endereço tenha permanecido igual; ponteiros salvos não podem recuperar dados descartados.

Questão 3 de 10

Uma função deve produzir um resultado de texto gravável que sobreviva ao seu retorno. A função chamadora não fornece um buffer, e chamadas separadas devem manter resultados independentes. Qual solução de armazenamento é adequada?
  1. Retornar um arranjo local; a função chamadora guarda seu endereço.
  2. Retornar um buffer estático; a função chamadora o libera.
  3. Retornar o literal; a função chamadora altera suas letras.
  4. Retornar uma cópia alocada; a função chamadora a libera.
Mostrar respostaOcultar resposta
Resposta: D
Uma cópia alocada tem tempo de vida além da chamada que a criou e fornece armazenamento independente para cada resultado. Um buffer estático também sobrevive, mas compartilha um resultado entre chamadas e não pode ser liberado pela função chamadora. Um arranjo local deixa de existir no retorno, e um literal de cadeia de caracteres não pode fornecer armazenamento gravável.

Questão 4 de 10

Um arranjo precisa de 13 valores int. Nesse sistema, sizeof(int) é 4. Quantos bytes a alocação deve solicitar?
  1. 52 bytes
  2. 13 bytes
  3. 17 bytes
  4. 104 bytes
Mostrar respostaOcultar resposta
Resposta: A
A solicitação precisa de 13 elementos de 4 bytes cada, totalizando 52 bytes. Uma solicitação de 13 bytes confunde a quantidade de elementos com a quantidade de bytes; ela não comporta treze valores int nesse sistema.

Questão 5 de 10

Uma função aloca um arranjo de ponteiros e quatro blocos separados para linhas. As cinco alocações são bem-sucedidas, e a função é responsável por todas elas. Como ela deve liberar a grade?
  1. Liberar o arranjo de ponteiros e depois ler seus ponteiros para linhas.
  2. Liberar apenas o arranjo de ponteiros; ele também é responsável pelas linhas.
  3. Liberar as quatro linhas e depois o arranjo de ponteiros.
  4. Liberar cada linha duas vezes e depois o arranjo de ponteiros.
Mostrar respostaOcultar resposta
Resposta: C
As linhas são quatro alocações separadas, e seus endereços ficam armazenados no arranjo externo. Libere-as enquanto esses endereços ainda estão disponíveis e depois libere o bloco externo. Liberar apenas o arranjo de ponteiros deixa as alocações das linhas sem liberação; liberá-lo primeiro também torna inválidas as leituras posteriores de suas entradas.

Questão 6 de 10

Um redimensionamento de tamanho positivo usa int *tmp = realloc(data, bytes); e retorna NULL. O bloco original era válido. O que o responsável pode fazer agora?
  1. Continuar usando ou liberar o bloco original inalterado.
  2. Usar os novos bytes por meio de tmp após redefini-lo.
  3. Liberar tmp para liberar imediatamente o bloco original.
  4. Usar data somente depois de alocá-lo novamente.
Mostrar respostaOcultar resposta
Resposta: A
Uma chamada malsucedida a realloc com tamanho positivo deixa a alocação original e seu conteúdo inalterados. Como o resultado foi recebido em tmp, data ainda contém seu endereço válido. Liberar o ponteiro temporário nulo não faz nada; isso não pode liberar o bloco ainda sob responsabilidade do programa identificado por data.

Questão 7 de 10

Um arranjo dinâmico começa com comprimento 0 e capacidade 3. Ele dobra a capacidade apenas quando está cheio. Após oito inserções bem-sucedidas, quais são seu comprimento e sua capacidade?
  1. Comprimento 8, capacidade 8
  2. Comprimento 12, capacidade 12
  3. Comprimento 8, capacidade 9
  4. Comprimento 8, capacidade 12
Mostrar respostaOcultar resposta
Resposta: D
A quarta inserção aumenta a capacidade de 3 para 6, e a sétima a aumenta de 6 para 12. Oito valores estão em uso, então o comprimento é 8. A capacidade 8 descreveria um crescimento exato, não a regra de duplicação estabelecida; a capacidade livre não aumenta a quantidade de valores armazenados.

Questão 8 de 10

Após uma alocação bem-sucedida, o código executa int *view = owner; free(owner); owner = NULL;. O que acontece se ele ler view[0] em seguida?
  1. Ele lê armazenamento válido porque view não é nulo.
  2. Ele acessa um objeto cujo tempo de vida terminou.
  3. Ele lê zero porque owner foi redefinido como nulo.
  4. Ele cria uma nova alocação no endereço anterior.
Mostrar respostaOcultar resposta
Resposta: B
view era uma referência alternativa à mesma alocação, cujo tempo de vida terminou em free(owner). Atribuir NULL a owner atualiza apenas essa variável ponteiro. Isso não altera view nem restaura o bloco antigo, então a leitura posterior é uso após a liberação com comportamento indefinido.

Questão 9 de 10

Um programa copia a cadeia de caracteres “network” com strcpy para um buffer recém-alocado no heap. Qual é a menor alocação necessária em bytes de C?
  1. 7 bytes
  2. 9 bytes
  3. 16 bytes
  4. 8 bytes
Mostrar respostaOcultar resposta
Resposta: D
As sete letras precisam de sete bytes, e strcpy também armazena o caractere nulo terminador. Portanto, o menor buffer tem oito bytes. Sete bytes comportariam as letras visíveis, mas deixariam o terminador fora da alocação, causando uma escrita fora dos limites mesmo que a impressão parecesse funcionar.

Questão 10 de 10

Uma variável global int *saved inicialmente é igual a NULL. Uma função auxiliar executa este código e retorna. Valores int ocupam 4 bytes. O Memcheck relata uma escrita inválida imediatamente após a alocação. Qual erro de percurso e categoria de vazamento ao término resultam disso?

int *p = malloc(7 * sizeof *p);
if (p == NULL) return;
for (size_t i = 0; i <= 7; ++i) p[i] = (int)i;
saved = p;
  1. Um valor int adicional é escrito; o bloco está ainda acessível.
  2. Quatro valores int adicionais são escritos; o bloco está definitivamente perdido.
  3. Um valor int adicional é escrito; o bloco está definitivamente perdido.
  4. Quatro valores int adicionais são escritos; o bloco está ainda acessível.
Mostrar respostaOcultar resposta
Resposta: A
O bloco de sete valores int permite índices de zero a seis, mas o laço também escreve no índice sete. O tamanho do acesso é de um valor int de quatro bytes. A variável global mantém o endereço inicial após o retorno da função auxiliar, então o bloco não liberado está ainda acessível, não definitivamente perdido.
Conjunto 2: Acompanhe caminhos de alocação e liberação de recursos10 Questões de Prática

Salve esta aula para ver todas as respostas e praticar cada conjunto.

Questão 1 de 10

Uma função declara static int visits = 0; e executa return ++visits;. Qual valor sua terceira chamada retorna?
  1. 1
  2. 3
  3. 0
  4. 2
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 2 de 10

Uma função aloca o buffer a, verifica o resultado e depois tenta alocar b. A segunda alocação falha. Ela é responsável por a e retornará uma falha imediatamente. Qual liberação de recursos é necessária?
  1. Liberar b e retornar sem alterar a.
  2. Atribuir nulo a a e depois retornar uma falha.
  3. Retornar uma falha; o fim do ponteiro local libera a.
  4. Liberar a antes de retornar uma falha.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 3 de 10

Uma função auxiliar retorna um arranjo alocado com sucesso por meio de um ponteiro local p. A função chamadora armazena o resultado em values. O que permanece válido imediatamente após o retorno da função auxiliar?
  1. A variável p da função auxiliar e o arranjo alocado.
  2. A variável p da função auxiliar e todos os seus parâmetros.
  3. O arranjo alocado e a variável values da função chamadora.
  4. A variável values da função chamadora, mas nenhum arranjo alocado.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 4 de 10

Para uma quantidade n diferente de zero, qual condição detecta, antes da alocação, que n * sizeof *p não cabe em size_t?
  1. n > SIZE_MAX / sizeof *p
  2. n * sizeof *p > SIZE_MAX
  3. sizeof p > n / SIZE_MAX
  4. n > SIZE_MAX * sizeof *p
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 5 de 10

Um buffer de cadeia de caracteres tem capacidade de 12 bytes e contém 11 caracteres mais seu terminador. Antes de acrescentar outro caractere, o que deve acontecer?
  1. Sobrescrever o terminador e manter o tamanho inalterado.
  2. Aumentar o comprimento duas vezes e crescer após a próxima inserção.
  3. Escrever no índice 12 e manter o terminador antigo no lugar.
  4. Aumentar o buffer e depois escrever o caractere e o terminador.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 6 de 10

Um laço aloca com sucesso 48 bytes em cada uma de 750 iterações e perde o único ponteiro de cada bloco sem liberá-lo. Quanto armazenamento é perdido por vazamento?
  1. 36.000 bytes
  2. 750 bytes
  3. 48.000 bytes
  4. 36.048 bytes
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 7 de 10

Um arranjo alocado com sucesso comporta nove elementos. Qual é seu maior índice válido de elemento?
  1. 9
  2. 8
  3. 10
  4. 7
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 8 de 10

Um programa começa a encerrar inesperadamente dentro de free após um laço que preenche um arranjo no heap. O que a investigação deve verificar antes de alterar a chamada de liberação?
  1. Escritas anteriores que possam ter ultrapassado os limites do arranjo.
  2. Se remover toda a liberação de recursos faz o programa terminar.
  3. Se uma impressão altera o endereço de liberação relatado.
  4. Se alocar o mesmo arranjo localmente esconde o encerramento.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 9 de 10

Uma alocação não liberada ainda tem seu endereço inicial armazenado em um ponteiro global ao término. Qual categoria de vazamento do Valgrind a descreve?
  1. Definitivamente perdida
  2. Indiretamente perdida
  3. Ainda acessível
  4. Possivelmente perdida
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 10 de 10

Um programa começa com este bloco verificado e depois toma uma decisão com base em p[5]. Um redimensionamento de tamanho positivo foi bem-sucedido. O ASan não relata nada, enquanto o Memcheck relata um valor não inicializado na condição. Qual correção corresponde ao histórico completo?

int *p = calloc(4, sizeof *p);
if (p == NULL) return EXIT_FAILURE;
int *tmp = realloc(p, 6 * sizeof *p);
if (tmp == NULL) { free(p); return EXIT_FAILURE; }
p = tmp;
  1. Reinicializar os elementos de 0 a 3 antes da condição.
  2. Inicializar o elemento 4, mas deixar o elemento 5 inalterado.
  3. Inicializar os elementos 4 e 6 antes da condição.
  4. Inicializar os elementos 4 e 5 antes de usar seus valores.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Conjunto 3: Crescimento, tempos de vida e evidências dos relatórios10 Questões de Prática

Salve esta aula para ver todas as respostas e praticar cada conjunto.

Questão 1 de 10

Após uma chamada bem-sucedida a calloc com tamanho positivo para um buffer de caracteres, qual cadeia de caracteres em C ele contém inicialmente?
  1. Uma cadeia vazia
  2. Uma cadeia de espaços
  3. Uma cadeia de “0”s
  4. Uma cadeia desconhecida
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 2 de 10

Uma função auxiliar tem um espaço de trabalho local fixo de 20 valores int, usado apenas durante a chamada, sem necessidade de redimensionamento. Qual escolha de armazenamento evita um responsável e um caminho de liberação de recursos desnecessários?
  1. Um bloco no heap cujo endereço é perdido.
  2. Um ponteiro retornado para um arranjo local.
  3. Um bloco no heap por trás de um global sem responsável.
  4. Um arranjo local comum de 20 valores int.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 3 de 10

Em um sistema com ponteiros de 8 bytes e valores int de 4 bytes, o código usa int *p = malloc(11 * sizeof p);. Quantos bytes ele solicita e o que há de errado na expressão?
  1. 44 bytes; ela usa o tamanho de um elemento em vez do ponteiro.
  2. 88 bytes; ela usa o tamanho do ponteiro em vez de um elemento.
  3. 11 bytes; sizeof elimina totalmente a multiplicação.
  4. 88 bytes; ela reserva onze blocos com responsáveis independentes.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 4 de 10

Um bloco de seis valores int está sob responsabilidade de p. O código salva o índice 4 e um ponteiro para esse elemento. Uma tentativa de crescimento para nove valores int falha usando um ponteiro temporário. Uma redução posterior para três é bem-sucedida, e o ponteiro responsável é atualizado. A seleção pode ser usada depois?
  1. Sim; o crescimento malsucedido preservou a seleção e o tempo de vida.
  2. Sim; reconstrua o índice 4 a partir do bloco final de três valores int.
  3. Não; o ponteiro antigo é inválido e o índice 4 não cabe mais.
  4. Não; o crescimento malsucedido já havia liberado o bloco de seis valores int.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 5 de 10

Um arranjo tem comprimento 6 e capacidade 8. Dois valores são acrescentados com sucesso. O código então salva o ponteiro responsável em old, define cap = 16 e atribui data = realloc(data, cap * sizeof *data). Essa solicitação de tamanho positivo falha antes que outro valor seja armazenado. Qual estado restaura o arranjo inalterado?
  1. Restaurar data = old, comprimento 9 e capacidade 16.
  2. Restaurar data = old, comprimento 8 e capacidade 8.
  3. Manter data = NULL, comprimento 8 e capacidade 8.
  4. Restaurar data = old, comprimento 8 e capacidade 16.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 6 de 10

Uma função mantém sua única alocação em buffer. Ela executa buffer = NULL; free(buffer); após seu último uso. Qual é o resultado para o bloco original?
  1. Ele é liberado porque a variável já conteve seu endereço.
  2. Ele é liberado e preenchido com bytes nulos pela atribuição.
  3. Ele permanece alocado e seu endereço é perdido.
  4. Ele permanece alocado, mas ainda pode ser alcançado por buffer.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 7 de 10

Após uma alocação verificada, uma função libera seu bloco por meio de p e depois chama free(alias), em que alias foi copiado de p antes da liberação. Que erro é esse?
  1. Uma liberação dupla por duas referências alternativas
  2. Um vazamento de dois blocos separados ainda válidos
  3. Uma liberação válida de dois ponteiros
  4. Uma leitura de valor não inicializado de armazenamento novo
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 8 de 10

Uma impressão de diagnóstico em stdout não termina com uma quebra de linha e, em seguida, o programa encerra inesperadamente. Seu texto nunca aparece. O que explica por que isso não permite determinar com confiança até onde a execução chegou?
  1. O texto ausente prova que a chamada de impressão nunca executou.
  2. A impressão necessariamente evitou o próprio erro de memória.
  3. A ausência de quebra de linha necessariamente causou a falha de memória.
  4. O texto ainda pode ter permanecido no buffer de saída.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 9 de 10

Uma pessoa desenvolvedora compilou program-asan com AddressSanitizer e quer uma execução independente do Memcheck. Qual executável deve ser fornecido ao Memcheck?
  1. A mesma versão com ASan e detecção de vazamentos desativada.
  2. Uma versão separada para depuração sem AddressSanitizer.
  3. A mesma versão com ASan e ponteiros de registro de ativação omitidos.
  4. Um arquivo-fonte fornecido diretamente em vez de um executável.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 10 de 10

O ASan para em items[n] = 0; imediatamente após uma alocação de n valores int. O laço usa i <= n, o resultado não é verificado e não há liberação de recursos. Qual correção trata a causa e os demais defeitos de responsabilidade pela liberação?
  1. Verificar a alocação, usar i < n no laço e liberar após o uso.
  2. Alocar n + 1 valores int, manter o limite e omitir a liberação.
  3. Verificar a alocação, manter i <= n e liberar antes do laço.
  4. Manter o tamanho e o limite, preencher o bloco com zeros e liberar após o uso.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Conjunto 4: Corrija a responsabilidade pela liberação em condições variáveis10 Questões de Prática

Salve esta aula para ver todas as respostas e praticar cada conjunto.

Questão 1 de 10

Qual é a duração de armazenamento de um arranjo global comum com escopo de arquivo?
  1. Duração de armazenamento estática
  2. Duração de armazenamento automática
  3. Duração de armazenamento alocada
  4. Uma nova duração por chamada
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 2 de 10

Uma biblioteca alocou quatro contadores inteiros com calloc e depois aumentou o bloco com sucesso para sete contadores. O que deve acontecer antes de incrementar o contador 6 pela primeira vez?
  1. Incrementá-lo diretamente porque todo crescimento preserva o preenchimento com zeros.
  2. Liberar e recriar os sete contadores para preservar os valores antigos.
  3. Atribuir a ele o valor inicial desejado antes de incrementá-lo.
  4. Definir seu endereço como NULL antes de usar a alocação maior.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 3 de 10

O arranjo externo de uma grade contém ponteiros para linhas com responsabilidade de liberação separada. Quem criou a grade já fez as linhas 0 e 1. Uma função de leitura toma a linha 0 emprestada até retornar. A criação da linha 2 falha enquanto essa função está ativa. As entradas posteriores nunca receberam valores. Qual liberação em caso de falha preserva todos os tempos de vida?
  1. Liberar as linhas 0 e 1, deixar a função de leitura terminar e depois liberar o arranjo externo.
  2. Deixar a função de leitura terminar, liberar o arranjo externo e depois ler e liberar suas linhas.
  3. Deixar a função de leitura terminar, liberar todas as linhas planejadas e depois liberar o arranjo externo.
  4. Deixar a função de leitura terminar, liberar as linhas 0 e 1 e depois liberar o arranjo externo.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 4 de 10

Uma alocação para quatro valores int está sob responsabilidade de p. Uma tentativa de redimensionamento para sete valores int falha usando tmp, deixando p inalterado. O código então executa p = realloc(p, 9 * sizeof *p), e essa solicitação também falha. Não existem referências alternativas. Qual alocação está perdida agora?
  1. O primeiro bloco solicitado, de sete valores int, permanece alocado, mas está perdido.
  2. O bloco original de quatro valores int permanece alocado, mas está perdido.
  3. O segundo bloco solicitado, de nove valores int, permanece alocado, mas está perdido.
  4. Nenhum bloco permanece alocado porque as duas solicitações de redimensionamento falharam.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 5 de 10

A capacidade de um arranjo dinâmico dobra a partir de 5 à medida que ele fica cheio. Ele começa vazio com essa capacidade e recebe 18 valores. Se cada crescimento mover o bloco, qual será a capacidade e o total máximo de cópias de elementos resultante do crescimento?
  1. Capacidade 20; no máximo 15 elementos copiados.
  2. Capacidade 20; no máximo 18 elementos copiados.
  3. Capacidade 18; no máximo 23 elementos copiados.
  4. Capacidade 40; no máximo 35 elementos copiados.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 6 de 10

Uma função aloca um resultado, preenche-o, libera-o e depois retorna o ponteiro à função chamadora. A função chamadora precisa ler esse resultado. Qual correção de responsabilidade pela liberação é adequada?
  1. Retornar o resultado liberado e pedir à função chamadora que verifique se não é nulo.
  2. Atribuir nulo ao ponteiro antes do retorno e manter a mesma interface.
  3. Liberar o resultado duas vezes para não deixar uma alocação inválida.
  4. Retornar o resultado ainda válido e deixar que a função chamadora o libere depois.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 7 de 10

Um código aloca char **labels = malloc(4 * sizeof *labels);, verifica o resultado e depois executa imediatamente strcpy(labels[0], "east");. Qual solução corrige a primeira entrada e deixa clara a responsabilidade por sua liberação?
  1. Alocar cinco posições para ponteiros e depois copiar pela entrada 0 como está.
  2. Definir a entrada 0 como o literal “east” e depois sobrescrevê-lo com strcpy.
  3. Alocar cinco valores char para a entrada 0, verificar, copiar e liberá-los depois.
  4. Definir a entrada 0 como NULL e depois copiar cinco valores char por essa entrada.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 8 de 10

Um redimensionamento bem-sucedido reduz um bloco de nove valores int inicializados para quatro. Qual conteúdo tem sua preservação garantida?
  1. Os valores originais nos índices de 5 a 8.
  2. Os valores originais nos índices de 0 a 3.
  3. Os nove valores, desde que nenhuma nova alocação ocorra.
  4. Quatro novos valores zero, substituindo a parte inicial original.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 9 de 10

Um teste apresenta um acesso de um elemento além dos limites de um arranjo local, que permanece dentro de memória da pilha válida. Uma execução do Memcheck não o detecta. Qual próxima verificação melhor corresponde a essa evidência?
  1. Repetir o Memcheck com as categorias de vazamento mostradas.
  2. Recompilar com ASan e executar novamente a mesma entrada.
  3. Tratar o acesso como válido depois de uma execução sem erros.
  4. Substituí-lo por um bloco maior no heap sem verificação.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 10 de 10

Um verificador relata uma liberação dupla. Remover as duas chamadas de liberação faz a execução parar de encerrar inesperadamente, mas deixa uma alocação sem liberação. Qual correção deve substituir essa alteração?
  1. Não manter nenhuma liberação e depender do sistema operacional ao término.
  2. Manter as duas liberações, mas atribuir nulo a uma referência alternativa emprestada.
  3. Mover as duas liberações para antes do último uso para reduzir o tempo de vida.
  4. Manter uma liberação após o último uso, sob um único responsável.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Conjunto 5: Rastreie históricos completos de memória10 Questões de Prática

Salve esta aula para ver todas as respostas e praticar cada conjunto.

Questão 1 de 10

Uma função retorna o endereço de um arranjo local de caracteres. Às vezes, a função chamadora imprime o texto esperado e, outras vezes, encerra inesperadamente. O que explica os dois resultados?
  1. O arranjo local tornou-se estático porque seu endereço saiu da função.
  2. O retorno transferiu a responsabilidade por uma alocação válida no heap.
  3. O texto esperado prova que o arranjo estava válido naquela execução.
  4. O arranjo deixou de existir no retorno; o acesso da função chamadora é inválido.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 2 de 10

Para um double *samples comum, o que sizeof samples mede?
  1. O tamanho de um valor double apontado.
  2. A quantidade de bytes de toda a alocação.
  3. O tamanho da variável ponteiro.
  4. A quantidade de elementos inicializados do arranjo.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 3 de 10

Uma função retorna uma cadeia de caracteres recém-alocada. A função chamadora a passa para uma função de impressão que apenas a toma emprestada; a função de impressão a libera por engano. A função chamadora então a lê e a libera. Qual mudança na responsabilidade pela liberação corrige o caminho completo?
  1. Remover a liberação da função chamadora; manter sua leitura após a impressão.
  2. Remover a liberação da função de impressão; manter a liberação final da chamadora.
  3. Manter as duas liberações; atribuir nulo apenas ao ponteiro local da função de impressão.
  4. Mover a liberação da chamadora para antes da impressão para encerrar a responsabilidade mais cedo.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 4 de 10

O Memcheck relata que um desvio condicional depende de um valor não inicializado. Qual opção ajuda a localizar onde esse valor se originou?
  1. --track-origins=yes
  2. --leak-check=no
  3. --show-leak-kinds=all
  4. --error-limit=no
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 5 de 10

Um buffer de texto começa com capacidade 6, comprimento 4 e um terminador no índice 4. Ele acrescenta X com sucesso sem crescer, atualizando seu terminador. O próximo caractere Y provoca um crescimento por meio de um ponteiro temporário, mas a solicitação falha. A API promete preservar a cadeia em caso de falha. Qual estado permanece?
  1. Comprimento 4, capacidade 6; X e Y são descartados.
  2. Comprimento 5, capacidade 6; X permanece e o terminador está em 5.
  3. Comprimento 6, capacidade 6; Y substitui o terminador em 5.
  4. Comprimento 5, capacidade 12; X permanece e o terminador está em 5.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 6 de 10

Um serviço processa três tarefas. Cada tarefa aloca com sucesso 24 bytes na variável local a e 40 bytes na variável local b. No caminho de erro, ela libera a, executa b = NULL e retorna. Não restam referências alternativas. Qual é o tamanho total das alocações perdidas e que mudança evita a perda?
  1. 192 bytes; liberar as duas variáveis ponteiro após seu retorno.
  2. 72 bytes; deixar de liberar a no caminho de erro.
  3. 40 bytes; liberar apenas b da última tarefa após o laço.
  4. 120 bytes; liberar cada bloco de b antes de redefinir seu ponteiro.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 7 de 10

Um arranjo tem um ponteiro emprestado para o elemento 2. O código aumenta o arranjo com sucesso por meio de um ponteiro temporário e atualiza o ponteiro responsável, depois escreve por meio do ponteiro emprestado antigo. O endereço não mudou. Qual regra ainda torna esse acesso inválido?
  1. Todo redimensionamento descarta o conteúdo da parte inicial original.
  2. O ponteiro responsável e o emprestado devem ter endereços diferentes.
  3. O redimensionamento bem-sucedido encerrou o tempo de vida do objeto antigo.
  4. O crescimento mantém apenas valores alcançados pelo índice zero.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 8 de 10

Valores int ocupam 4 bytes e ponteiros ocupam 8 bytes. O código usa int *p = malloc(6 * sizeof p);, verifica o sucesso, inicializa os índices de 0 a 5 e depois soma os índices de 0 a 11. Qual diagnóstico corresponde à soma inteira?
  1. Suas últimas seis leituras estão dentro dos limites, mas não foram inicializadas.
  2. Suas últimas seis leituras estão fora de uma alocação de 24 bytes.
  3. As doze leituras são válidas porque a alocação as inicializa.
  4. Apenas sua leitura final ultrapassa o fim da alocação maior que o necessário.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 9 de 10

Um programa para com uma falha de segmentação em uma linha que lê p[index]. O que deve ser verificado sobre esse acesso?
  1. A origem do ponteiro, o tempo de vida do objeto apontado e a faixa do índice.
  2. Apenas o índice, porque qualquer ponteiro não nulo é válido.
  3. Apenas a linha de liberação, porque todas as falhas são liberações duplas.
  4. Apenas o armazenamento da saída em buffer, porque uma leitura de arranjo não pode falhar.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 10 de 10

Um relatório de ferramenta mostra uma leitura em display, uma liberação anterior em reset e a alocação em load. A função chamadora invoca load, reset e depois display no mesmo resultado. Ela precisa dessa exibição. Qual correção decorre do histórico do bloco?
  1. Aumentar o tamanho em load, mantendo a ordem das chamadas.
  2. Atribuir nulo apenas ao ponteiro local de reset e manter a leitura posterior.
  3. Manter o resultado válido durante display e depois liberá-lo.
  4. Ignorar reset porque o erro relatado ocorreu em display.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Travou em uma questão sua? Ajuda com Tarefas

Termos-Chave

63 Termos-Chave, cada um com sua definição

A

Acesso fora dos limites de um buffer no heap
Um acesso fora dos limites de um buffer no heap lê ou escreve antes do início ou depois do fim de um bloco alocado, mesmo que o endereço ao redor seja acessível.
Um laço que alcança o índice dez em uma alocação de dez elementos ultrapassa a faixa válida de elementos do bloco.
AddressSanitizer
O AddressSanitizer é uma instrumentação acrescentada pelo compilador para detectar acessos e liberações de memória inválidos enquanto um executável instrumentado é executado.
Uma versão com ASan pode identificar um acesso além dos limites de um arranjo local que permanece dentro de memória da pilha válida.
Ainda acessível
Uma alocação está ainda acessível no Valgrind quando um ponteiro permanece ao término, embora o programa não tenha liberado o bloco.
Um ponteiro global que mantém o endereço inicial de uma alocação não liberada torna o bloco acessível à verificação no término.
Alocação
Uma alocação é uma solicitação de um bloco reservado de memória; uma solicitação bem-sucedida fornece seu endereço inicial para uso e liberação posteriores.
Um programa verifica se sua alocação foi bem-sucedida antes de armazenar o primeiro elemento do arranjo solicitado.
Alocação aninhada
Uma alocação aninhada é uma solução com blocos internos alocados separadamente, cujos endereços são mantidos em um contêiner externo alocado.
Uma grade com linhas separadas precisa que as linhas sejam liberadas antes do arranjo externo de ponteiros.
Alocação perdida
Uma alocação perdida é um bloco ainda reservado cujo endereço utilizável não é mais mantido, impedindo que o programa o libere normalmente.
Atribuir NULL ao único ponteiro responsável sem chamar free primeiro faz perder o endereço da alocação.
Armazenamento automático
O armazenamento automático pertence a objetos locais comuns e parâmetros cujos tempos de vida terminam quando a execução sai de seu bloco.
Um arranjo local de uma função auxiliar deixa de existir quando ela retorna, mesmo que a função chamadora tenha guardado seu endereço.
Armazenamento da saída em buffer
O armazenamento da saída em buffer mantém temporariamente na memória o texto impresso antes de gravá-lo, então um encerramento inesperado pode impedir que mensagens recentes de progresso apareçam.
Chamar fflush(stdout) após uma impressão de progresso disponibiliza o texto armazenado em buffer antes da próxima operação.
Armazenamento estático
O armazenamento estático dura toda a execução do programa e é usado por objetos globais e variáveis locais declaradas com static.
Um contador estático dentro de uma função mantém seu valor atualizado quando a próxima chamada começa.
Arranjo de tamanho variável (VLA)
Um arranjo de tamanho variável é um arranjo com tamanho definido durante a execução e tempo de vida automático, que geralmente ocupa armazenamento na pilha.
Declarar int a[n] não fornece a verificação de falha por resultado nulo disponível na alocação no heap.
Arranjo dinâmico
Um arranjo dinâmico é um armazenamento alocado para elementos, acompanhado por um comprimento e uma capacidade para que possa se expandir quando mais valores chegarem.
Um arranjo dinâmico pode conter dez valores em dezesseis posições, deixando capacidade livre para inserções posteriores.

Á

Área de memória dinâmica (heap)
O heap é a área de memória da qual o alocador fornece blocos cujos tempos de vida são controlados por alocação e desalocação.
Retornar o endereço de um arranjo alocado deixa o bloco no heap para que a função chamadora o use e libere.

B

Buffer estático compartilhado
Um buffer estático compartilhado é um armazenamento persistente de resultado de uma função, reutilizado pelas chamadas, de modo que uma chamada posterior pode sobrescrever um resultado anterior.
Dois ponteiros retornados por chamadas sucessivas podem se referir ao mesmo arranjo estático em vez de textos independentes.
Buffer fornecido pela função chamadora
Um buffer fornecido pela função chamadora é um armazenamento passado a uma função para sua saída, permitindo que a função chamadora mantenha o controle de seu tamanho e tempo de vida.
Uma função chamadora pode passar seu próprio arranjo de 16 bytes a uma função auxiliar que o preenche com uma saudação curta.

C

calloc
A função calloc reserva um arranjo usando uma quantidade e um tamanho de elemento, preenche seus bytes com zeros e retorna um ponteiro nulo se a alocação falhar.
Contadores inteiros obtidos por uma solicitação verificada a calloc começam em zero antes do primeiro incremento.
Caminho de liberação de recursos
Um caminho de liberação de recursos é um código que libera recursos obtidos antes da saída de uma função, inclusive quando o trabalho para após sucesso parcial.
Se uma segunda alocação falhar, o caminho de erro libera o primeiro bloco antes de retornar uma falha.
Capacidade
A capacidade de um arranjo é a quantidade de posições de elementos reservadas em seu bloco, incluindo posições não utilizadas que ainda não pertencem ao seu conteúdo lógico.
Uma solicitação de crescimento malsucedida deve deixar a capacidade inalterada porque nenhuma posição adicional foi obtida.
Comportamento indefinido
O comportamento indefinido ocorre quando C não impõe requisitos ao resultado do programa após uma violação de regra, permitindo encerramentos inesperados ou corrupção silenciosa.
Uma escrita fora dos limites pode parecer inofensiva em um teste e corromper outro valor com uma entrada diferente.
Comprimento
O comprimento de um arranjo é sua quantidade de elementos inicializados atualmente em uso, que determina a faixa para cálculos sobre seus valores armazenados.
Após acrescentar um quinto valor ao armazenamento disponível, o comprimento passa a ser cinco, mesmo que a capacidade permaneça em oito.
Crescimento geométrico
O crescimento geométrico aumenta a capacidade por um fator multiplicativo, como a duplicação, para limitar as cópias repetidas ao longo de muitas inserções.
Um arranjo cheio com capacidade quatro cresce para oito e depois para dezesseis quando seu próximo limite de crescimento é alcançado.

D

Definitivamente perdida
Uma alocação está definitivamente perdida no Memcheck quando não resta nenhum ponteiro, de modo que seu armazenamento reservado não pode ser liberado por meio de um endereço armazenado.
Uma função auxiliar que abandona seu único ponteiro responsável pode deixar um bloco definitivamente perdido no relatório de término.
Duração de armazenamento
A duração de armazenamento de um objeto determina por quanto tempo seu armazenamento fica reservado, incluindo se ele termina com um bloco, com a desalocação ou com o término do programa.
Um ponteiro local e seu arranjo alocado podem ter durações de armazenamento diferentes mesmo quando compartilham um endereço.

E

Entrada que reproduz o problema
Uma entrada que reproduz o problema é um caso de teste que alcança o problema observado de forma confiável, fornecendo um ponto de partida concreto para depuração e verificação.
Reduza uma entrada que falha mantendo seu erro para facilitar a interpretação do primeiro relatório relevante.
Erro de uma unidade
Um erro de uma unidade torna um limite uma posição maior ou menor que o necessário, frequentemente incluindo a quantidade de elementos como um índice final inválido.
Alterar i <= n para i < n faz um percurso iniciado em zero visitar exatamente os n elementos pretendidos.
Estouro de pilha
Um estouro de pilha esgota o espaço disponível para chamadas ativas e objetos automáticos, frequentemente por recursão profunda ou um arranjo local enorme.
Uma função recursiva sem otimização e sem condição de parada pode continuar acrescentando registros de ativação até falhar.
Estouro no cálculo de tamanho
Um estouro no cálculo de tamanho ocorre quando um cálculo que transforma uma quantidade em bytes ultrapassa a faixa de size_t e sofre redução modular para um valor representável menor.
Compare a quantidade de elementos com SIZE_MAX / sizeof *p antes de multiplicá-la pelo tamanho do elemento.
Execução de verificação
Uma execução de verificação executa o programa corrigido com um verificador para confirmar o caminho testado, seguida por entradas que exercitam outros caminhos.
Após uma execução sem erros do caso de reprodução, teste entrada vazia, limites de crescimento e saídas antecipadas para identificar liberações de recursos ausentes.

F

Falha de segmentação
Uma falha de segmentação é uma falha do sistema operacional causada por um acesso proibido à memória, como uma desreferenciação de ponteiro nulo ou uma escrita em área somente de leitura.
Uma escrita inválida pode corromper dados primeiro e causar uma falha de segmentação posteriormente em outra função.
free
A função free libera um bloco alocado ainda válido para reutilização quando recebe seu endereço inicial e não faz nada quando recebe NULL.
A função chamadora libera uma cópia de cadeia de caracteres retornada somente depois que todos que a tomaram emprestada terminam a leitura.

I

Indiretamente perdida
Uma alocação está indiretamente perdida quando só pode ser alcançada por meio de outro bloco que também foi perdido, conforme relatado pelo Memcheck.
Perder um arranjo externo com endereços de linhas sob sua responsabilidade também pode deixar as alocações das linhas indiretamente perdidas.
Inicialização
A inicialização estabelece um valor antes de sua leitura, por meio de uma atribuição inicial ou de uma alocação adequadamente preenchida com zeros.
Um contador deve ter um valor inicial definido porque incrementá-lo primeiro lê seu valor anterior.
Instrumentação
A instrumentação acrescenta código de verificação durante a compilação para que o programa resultante possa detectar erros selecionados enquanto seus caminhos testados executam.
Acrescentar -fsanitize=address durante a compilação e a ligação cria um executável instrumentado com ASan.

L

LeakSanitizer
O LeakSanitizer verifica alocações que vazaram ao término e pode acompanhar o ASan; sua disponibilidade e suas configurações padrão dependem da plataforma.
Versões com ASan no Linux normalmente ativam verificações de vazamento; algumas cadeias de ferramentas do macOS não podem fornecê-las.
Leitura de valor não inicializado
Uma leitura de valor não inicializado consome um valor que nunca foi definido, inclusive em um elemento dentro dos limites de um armazenamento recém-alocado ou aumentado.
Somar três valores int alocados após atribuir valores apenas a dois ainda lê um terceiro valor não definido.
Liberação dupla
Uma liberação dupla tenta liberar um bloco cujo tempo de vida já terminou por uma liberação anterior, inclusive por meio de outra referência alternativa.
Um responsável e quem toma um ponteiro emprestado não podem chamar free cada um sobre o que era uma única alocação válida compartilhada.
Literal de cadeia de caracteres
Um literal de cadeia de caracteres é um texto escrito entre aspas no código-fonte em C, com armazenamento estático que não deve ser modificado por meio de um ponteiro.
Use char word[] = "hello"; quando for necessária uma cópia local gravável em vez de um ponteiro para o literal.
Local de alocação
Um local de alocação é a posição do programa que solicita um bloco, mostrada nos relatórios de ferramentas para estabelecer onde esse armazenamento começou.
O local de alocação em um relatório de vazamento pode estar dentro de uma função auxiliar, enquanto a liberação correta cabe à função chamadora.

M

malloc
A função malloc reserva uma quantidade solicitada de bytes e retorna o endereço de um bloco ou um ponteiro nulo, sem inicializar os valores armazenados.
Um arranjo de inteiros obtido com malloc precisa receber atribuições antes que seus elementos sejam somados ou comparados.
Memcheck
O Memcheck é o verificador de memória padrão do Valgrind, que acompanha acessos inválidos, tempos de vida de alocações, valores não definidos e blocos não liberados durante uma execução.
Execute um executável comum para depuração no Memcheck para obter linhas do código-fonte junto aos históricos de memória.
Memória preenchida com bits zero
A memória preenchida com bits zero tem todos os bits definidos como zero, produzindo zero inteiro, mas sem garantir de forma portável zero de ponto flutuante ou ponteiros nulos.
Um buffer de caracteres de tamanho positivo obtido de calloc começa com um caractere nulo e contém uma cadeia vazia.
MemorySanitizer
O MemorySanitizer é o verificador do Clang para uso de valores não inicializados em sistemas compatíveis, tratando um tipo de erro que o ASan comum não acompanha.
Use-o em uma versão separada e compatível, em vez de combinar sua instrumentação com a do AddressSanitizer.

P

Pilha de chamadas
A pilha de chamadas é a região comumente usada para acompanhar chamadas de função ativas, seus objetos locais, parâmetros e informações de retorno.
Uma função recursiva sem otimização pode continuar acrescentando registros de ativação até esgotar o espaço disponível na pilha.
Ponteiro emprestado
Um ponteiro emprestado dá acesso a um objeto ainda válido enquanto outra parte do programa permanece responsável por decidir quando liberá-lo.
Uma função de impressão pode tomar uma cadeia emprestada durante sua chamada sem adquirir permissão para liberar o bloco da função chamadora.
Ponteiro nulo
Um ponteiro nulo é um valor de ponteiro que não designa nenhum objeto, retornado por funções de alocação quando um bloco solicitado de tamanho diferente de zero não pode ser fornecido.
Uma verificação de ponteiro nulo envia a execução para um caminho de erro antes que o código tente acessar uma alocação malsucedida.
Ponteiro para o interior de um objeto
Um ponteiro para o interior de um objeto endereça uma posição dentro de um objeto, como um elemento de arranjo, em vez do endereço inicial de sua alocação.
Guarde um índice de elemento durante um redimensionamento bem-sucedido e reconstrua o ponteiro para o interior a partir do resultado.
Ponteiro pendente
Um ponteiro pendente refere-se a um objeto cujo tempo de vida terminou, sem deixar um objeto antigo válido para acesso posterior por esse ponteiro.
Um ponteiro para o antigo arranjo local de uma função auxiliar fica pendente imediatamente após seu retorno.
Ponteiro temporário
Um ponteiro temporário recebe um resultado de redimensionamento antes da atualização do ponteiro responsável, preservando o endereço da alocação antiga se a solicitação falhar.
Com tmp = realloc(p, bytes), um resultado nulo deixa p disponível para uso contínuo ou liberação de recursos.
Possivelmente perdida
Uma alocação está possivelmente perdida no Memcheck quando apenas um ponteiro para o interior foi encontrado, deixando incerteza sobre o endereço inicial da alocação.
Examine se um ponteiro que percorre o bloco substituiu o ponteiro responsável em vez de preservar um ponteiro inicial separado.
Primeiro erro relatado
O primeiro erro relatado é a primeira ocorrência apontada pelo verificador em uma execução, frequentemente o melhor ponto de partida porque falhas posteriores podem resultar de seu dano.
Corrija o primeiro acesso fora dos limites antes de investigar cada falha posterior do alocador no mesmo relatório.

R

Rastreamento da pilha de chamadas
Um rastreamento da pilha de chamadas lista as chamadas de função ativas em um encerramento inesperado ou erro, ajudando a localizar o acesso relevante e o caminho que levou até ele.
Comece pelo primeiro registro de ativação relevante em seu próprio código e depois examine como seu ponteiro recebeu o valor.
Rastreamento de origem
O rastreamento de origem acompanha um valor não inicializado de volta ao armazenamento que o forneceu, ajudando a localizar a atribuição de valor ausente.
A opção --track-origins=yes do Memcheck pode explicar por que uma condição posterior depende de dados não definidos no heap.
realloc
A função realloc substitui um bloco alocado por um objeto do tamanho solicitado, preservando a parte inicial que cabe e podendo alterar seu endereço.
Aumentar um bloco de quatro valores int para seis preserva os quatro primeiros valores, mas deixa dois novos elementos sem valores definidos.
Referência alternativa ao mesmo objeto
Uma referência alternativa é outro ponteiro para o mesmo objeto, que compartilha seu tempo de vida sem ser atualizado quando outra variável ponteiro muda.
Atribuir NULL ao ponteiro responsável após a liberação deixa quaisquer referências alternativas salvas inutilizáveis para leituras posteriores.
Responsabilidade pela liberação
A responsabilidade pela liberação é a obrigação de liberar um bloco alocado após seu último uso ou de transferir deliberadamente esse dever a outra parte do programa.
Uma função que produz uma cópia transfere a responsabilidade pela liberação à função chamadora quando retorna um resultado recém-alocado.
Resumo do heap
Um resumo do heap relata a atividade de alocação e o armazenamento restante no heap ao término, ajudando a investigar blocos que não foram liberados.
Leia os totais de blocos restantes junto com as categorias de vazamento, em vez de tratar cada bloco como perdido.
Reutilização de buffer
A reutilização de buffer mantém uma alocação adequada durante operações repetidas, substituindo seu conteúdo antes de liberar o bloco posteriormente.
Um laço pode reutilizar um espaço de trabalho de 100 bytes em vez de alocar e abandonar um bloco em cada passagem.

S

size_t
O tipo size_t é o tipo inteiro sem sinal usado para tamanhos de objetos e quantidades de bytes de alocação, incluindo o resultado de sizeof.
Um arranjo dinâmico usa variáveis size_t para seu comprimento e sua capacidade, para que possam descrever tamanhos de objetos.
sizeof
O operador sizeof fornece o tamanho de um tipo ou objeto em bytes de C; aplicado a um ponteiro, mede o ponteiro em vez da alocação para a qual ele aponta.
A solicitação n * sizeof *p acompanha o tipo do elemento, enquanto sizeof p não pode recuperar o comprimento do arranjo.

T

Tempo de vida do objeto
O tempo de vida de um objeto é o período durante o qual ele existe em armazenamento reservado e pode ser acessado por meio de ponteiros válidos para ele.
Uma chamada bem-sucedida a realloc encerra o tempo de vida do objeto antigo mesmo quando o endereço retornado parece inalterado.
Terminador nulo
Um terminador nulo é o caractere de valor zero que marca o fim de uma cadeia de caracteres em C e ocupa um byte além de seus caracteres visíveis.
A cadeia de quatro caracteres “west” precisa de cinco bytes quando copiada junto com seu terminador.

U

UndefinedBehaviorSanitizer
O UndefinedBehaviorSanitizer acrescenta verificações para comportamentos indefinidos selecionados que não são erros de endereço, como estouro de inteiro com sinal, e pode acompanhar o ASan.
Uma versão de desenvolvimento pode usar -fsanitize=address,undefined para ativar os dois conjuntos compatíveis de verificações.
Uso após a liberação
O uso após a liberação acessa um objeto alocado após sua desalocação, o que pode retornar dados que parecem antigos, corromper a memória ou encerrar o programa inesperadamente.
Imprimir uma cadeia liberada por meio de uma referência alternativa salva é inválido mesmo quando as palavras esperadas aparecem.

V

Vazamento de memória
Um vazamento de memória é uma alocação que não é mais necessária, mas permanece sem liberação, frequentemente porque o programa perdeu seu último ponteiro para ela.
Sobrescrever o único ponteiro responsável com uma nova alocação pode abandonar permanentemente o bloco antigo.

Sobre esta aula

Por StudeliRelatar um erro

Crie seus próprios Materiais de Estudo

Transforme suas anotações, slides e documentos em Guias de Estudo, Questões de Prática, Flashcards e mais 6 ferramentas.

Começar