- Aprender
- Computação e TI
- Programação
- Programação em C
- Gerenciamento de memória dinâmica em C
Gerenciamento de memória dinâmica em C
BásicoAula 5 de 8 do curso Curso de Programação em C
O que você vai aprender
- 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áticoDescreva 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 heapEscolha 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.
- Compare a pilha, o heap e o armazenamento estático
- 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 sizeofCalcule 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 callocUse 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çãoLibere 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.
- Solicite memória com malloc e sizeof
- 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 reallocReceba 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 crescerMantenha 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.
- Aumente e reduza um bloco com segurança usando realloc
- 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óriaExplique 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 duplasReconheç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 inicializadosMantenha 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çãoExplique o que é uma falha de segmentação, indique suas causas comuns e parta do encerramento inesperado para chegar à linha com defeito.
- Encontre e evite vazamentos de memória
- 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 ValgrindCompile 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 LeakSanitizerCompile 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 ferramentaSiga 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.
- Verifique um programa com o Memcheck do Valgrind
Guia de estudo de Gerenciamento de memória dinâmica em C
1Explique 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 armazenamento | Objetos típicos | Fim do tempo de vida |
|---|---|---|
| Automática | Variáveis locais comuns e parâmetros | Quando seu bloco termina |
| Estática | Variáveis globais e locais declaradas static | Quando o programa termina |
| Alocada | Blocos obtidos de funções de alocação | Quando 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
| Necessidade | Escolha inicial adequada |
|---|---|
| Um arranjo pequeno e fixo usado durante uma chamada | Arranjo local |
| Uma quantidade lida da entrada | Alocação no heap com verificação |
| Um resultado que sobreviva à função que o cria | Alocação no heap ou buffer de responsabilidade da função chamadora |
| Dados que crescem à medida que a entrada chega | Buffer redimensionável no heap |
| Um arranjo muito grande | Alocaçã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.
2Aloque 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 *pointerpara obter a quantidade de bytes de um arranjo. - Verifique o estouro da multiplicação antes de chamar
malloc. - Verifique se o resultado é
NULLantes de usá-lo. - Inicialize cada elemento de
mallocantes de lê-lo. - Guarde a quantidade de elementos separadamente;
sizeof pointernã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 inicial | Escolha natural |
|---|---|
| Incrementar contadores inteiros | calloc |
| Definir indicadores inteiros gradualmente a partir de um estado com todos os valores em zero | calloc |
| Sobrescrever imediatamente cada elemento | malloc |
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
callocuma 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
reallocnã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 parafree. - 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
freepara o bloco mais recente.
3Redimensione 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
freepara 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.
4Reconheç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
reallocinvalida 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
nelementos vão de zero an - 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.
5Encontre 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ório | Significado |
|---|---|
| Definitivamente perdida | Não resta nenhum ponteiro para o bloco |
| Indiretamente perdida | O bloco só é acessível por meio de outro bloco perdido |
| Possivelmente perdida | Apenas um ponteiro para o interior foi encontrado |
| Ainda acessível | Um 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=yespara 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
| Necessidade | Ferramenta útil |
|---|---|
| Verificações rápidas durante o desenvolvimento, incluindo limites de arranjos locais | Versão compilada com ASan |
| Rastreamento de valores não inicializados sem instrumentação de sanitizador | Memcheck em um sistema compatível |
| Vazamentos de alocação ao término | LeakSanitizer 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
1Acompanhe um objeto ao longo de chamadas de função
9 passos
- 1Introduçã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.
- 2Explicaçã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
staticdura 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.
- 3ExemploCrie 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
intocupa 4 bytes, o bloco tem 20 bytes. Verifiquepantes 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. - 4ExemploRetorne o endereço
A função auxiliar retorna
p, e sua função chamadora armazena o resultado emint *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,
numsainda 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. - 5DesafioO que sobrevive ao retorno?
Uma função auxiliar cria uma variável automática
int counte 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
countda função auxiliar deixou de existir no retorno. O endereço copiado alcança apenas o arranjo; guardar um endereço não preservariacountse a função auxiliar tivesse retornado&count. - 6ExemploTente retornar um arranjo local
Agora altere a função auxiliar para usar
int values[5];e retornarvalues. 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.
- 7Conexã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.
- 8DesafioDois 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.
- 9Recapitulaçã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.
2Aloque uma grade e libere recursos em cada caminho
13 passos
Salve esta aula para seguir cada Lição Interativa passo a passo.
- 1IntroduçãoPlaneje tanto o sucesso quanto a falha
- 2ExplicaçãoTransforme quantidades em bytes
- 3DicaProteja antes de multiplicar
- 4DesafioTamanho do elemento ou do ponteiro?
- 5ExplicaçãoDê um valor inicial aos contadores
- 6ExemploUse a linha preenchida com zeros
- 7DesafioE quanto ao crescimento posterior?
- 8ExplicaçãoConte blocos, não nomes de ponteiros
- 9ExemploInterrompa a construção em caso de falha parcial
- 10DesafioEscolha a ordem de liberação
- 11Ideia-chaveUm responsável pode emprestar ponteiros
- 12DesafioUm caminho para antecipadamente
- 13RecapitulaçãoPlaneje a liberação junto com a construção
3Acrescente valores sem perder dados
12 passos
Salve esta aula para seguir cada Lição Interativa passo a passo.
- 1IntroduçãoAumente antes da próxima gravação
- 2ExplicaçãoO redimensionamento pode substituir o objeto
- 3ExemploSepare falha de sucesso
- 4DesafioO que pode ser preservado após a redução?
- 5ExplicaçãoComprimento e capacidade respondem a perguntas diferentes
- 6ExemploUltrapasse o primeiro limite de crescimento
- 7ExemploUltrapasse o próximo limite
- 8Ideia-chaveO espaço livre reduz a quantidade de cópias
- 9DesafioQual é o estado após uma inserção malsucedida?
- 10ConexãoUma cadeia de caracteres reserva uma posição para o terminador
- 11DesafioUma seleção emprestada passa por um crescimento
- 12RecapitulaçãoAtualize o estado somente após o sucesso
4Examine todo o histórico de um bloco
15 passos
Salve esta aula para seguir cada Lição Interativa passo a passo.
- 1IntroduçãoQuatro perguntas para cada acesso
- 2ExemploPerca uma alocação ao substituir seu endereço
- 3DesafioAtribuir nulo ao ponteiro libera a memória?
- 4DicaExamine as saídas e também os laços
- 5ExemploLibere uma vez e invalide todas as referências alternativas
- 6DesafioCorrija o último uso
- 7Ideia-chaveA saída esperada pode vir de código inválido
- 8ExemploConte o terminador nulo
- 9ExemploAlocado não significa inicializado
- 10DesafioIdentifique dois limites separados
- 11ExplicaçãoUma falha é um sintoma
- 12ExemploLeia o acesso que causou o encerramento
- 13DesafioUma impressão faz o encerramento desaparecer
- 14DesafioCombine responsabilidade pela liberação e limites
- 15RecapitulaçãoIdentifique a regra violada
5Leia um relatório e verifique a correção
14 passos
Salve esta aula para seguir cada Lição Interativa passo a passo.
- 1IntroduçãoTransforme uma mensagem no histórico de um bloco
- 2ExplicaçãoUse uma versão comum para depuração com o Memcheck
- 3ExemploInterprete a descrição do endereço
- 4DesafioLeia bytes como bytes
- 5DicaEscolha uma opção de relatório para o problema
- 6ExplicaçãoCompile um executável com ASan separadamente
- 7ExemploAcompanhe o uso após a liberação por três pilhas
- 8DesafioUma execução sem erros no ASan deixa passar um erro de valor
- 9ExemploRelacione o relatório ao laço
- 10ExemploMantenha o bloco válido durante o trabalho
- 11DesafioOnde um vazamento deve ser corrigido?
- 12ConexãoVerifique o término e o suporte da plataforma
- 13DesafioConclua o ciclo de correção
- 14RecapitulaçãoUse evidências, corrija e execute novamente
Questões de Gerenciamento de memória dinâmica em C
Conjunto 1: Tempos de vida, contagens de bytes e primeiras verificações10 Questões de Prática
Questão 1 de 10
int *hits = calloc(6, sizeof *hits);, qual é o valor de hits[4] antes de qualquer atribuição posterior?Mostrar respostaOcultar resposta
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
Mostrar respostaOcultar resposta
Questão 3 de 10
Mostrar respostaOcultar resposta
Questão 4 de 10
sizeof(int) é 4. Quantos bytes a alocação deve solicitar?Mostrar respostaOcultar resposta
Questão 5 de 10
Mostrar respostaOcultar resposta
Questão 6 de 10
int *tmp = realloc(data, bytes); e retorna NULL. O bloco original era válido. O que o responsável pode fazer agora?Mostrar respostaOcultar resposta
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
Mostrar respostaOcultar resposta
Questão 8 de 10
int *view = owner; free(owner); owner = NULL;. O que acontece se ele ler view[0] em seguida?Mostrar respostaOcultar resposta
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
strcpy para um buffer recém-alocado no heap. Qual é a menor alocação necessária em bytes de C?Mostrar respostaOcultar resposta
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;
Mostrar respostaOcultar resposta
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
static int visits = 0; e executa return ++visits;. Qual valor sua terceira chamada retorna?Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 2 de 10
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?Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 3 de 10
p. A função chamadora armazena o resultado em values. O que permanece válido imediatamente após o retorno da função auxiliar?Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 4 de 10
n diferente de zero, qual condição detecta, antes da alocação, que n * sizeof *p não cabe em size_t?Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 5 de 10
Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 6 de 10
Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 7 de 10
Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 8 de 10
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?Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 9 de 10
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;
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
calloc com tamanho positivo para um buffer de caracteres, qual cadeia de caracteres em C ele contém inicialmente?Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 2 de 10
Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 3 de 10
int *p = malloc(11 * sizeof p);. Quantos bytes ele solicita e o que há de errado na expressão?Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 4 de 10
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?Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 5 de 10
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?Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 6 de 10
buffer. Ela executa buffer = NULL; free(buffer); após seu último uso. Qual é o resultado para o bloco original?Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 7 de 10
p e depois chama free(alias), em que alias foi copiado de p antes da liberação. Que erro é esse?Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 8 de 10
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?Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 9 de 10
program-asan com AddressSanitizer e quer uma execução independente do Memcheck. Qual executável deve ser fornecido ao Memcheck?Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 10 de 10
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?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
Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 2 de 10
calloc e depois aumentou o bloco com sucesso para sete contadores. O que deve acontecer antes de incrementar o contador 6 pela primeira vez?Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 3 de 10
Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 4 de 10
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?Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 5 de 10
Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 6 de 10
Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 7 de 10
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?Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 8 de 10
Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 9 de 10
Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 10 de 10
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
Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 2 de 10
double *samples comum, o que sizeof samples mede?Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 3 de 10
Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 4 de 10
Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 5 de 10
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?Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 6 de 10
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?Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 7 de 10
Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 8 de 10
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?Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 9 de 10
p[index]. O que deve ser verificado sobre esse acesso?Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 10 de 10
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?Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Travou em uma questão sua? Ajuda com Tarefas
Termos-Chave
A
Acesso fora dos limites de um buffer no heap
AddressSanitizer
Ainda acessível
Alocação
Alocação aninhada
Alocação perdida
NULL ao único ponteiro responsável sem chamar free primeiro faz perder o endereço da alocação.Armazenamento automático
Armazenamento da saída em buffer
fflush(stdout) após uma impressão de progresso disponibiliza o texto armazenado em buffer antes da próxima operação.Armazenamento estático
static.Arranjo de tamanho variável (VLA)
int a[n] não fornece a verificação de falha por resultado nulo disponível na alocação no heap.Arranjo dinâmico
Á
Área de memória dinâmica (heap)
B
Buffer estático compartilhado
Buffer fornecido pela função chamadora
C
calloc
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.calloc começam em zero antes do primeiro incremento.Caminho de liberação de recursos
Capacidade
Comportamento indefinido
Comprimento
Crescimento geométrico
D
Definitivamente perdida
Duração de armazenamento
E
Entrada que reproduz o problema
Erro de uma unidade
i <= n para i < n faz um percurso iniciado em zero visitar exatamente os n elementos pretendidos.Estouro de pilha
Estouro no cálculo de tamanho
size_t e sofre redução modular para um valor representável menor.SIZE_MAX / sizeof *p antes de multiplicá-la pelo tamanho do elemento.Execução de verificação
F
Falha de segmentação
free
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.I
Indiretamente perdida
Inicialização
Instrumentação
-fsanitize=address durante a compilação e a ligação cria um executável instrumentado com ASan.L
LeakSanitizer
Leitura de valor não inicializado
Liberação dupla
free cada um sobre o que era uma única alocação válida compartilhada.Literal de cadeia de caracteres
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
M
malloc
malloc reserva uma quantidade solicitada de bytes e retorna o endereço de um bloco ou um ponteiro nulo, sem inicializar os valores armazenados.malloc precisa receber atribuições antes que seus elementos sejam somados ou comparados.Memcheck
Memória preenchida com bits zero
calloc começa com um caractere nulo e contém uma cadeia vazia.MemorySanitizer
P
Pilha de chamadas
Ponteiro emprestado
Ponteiro nulo
Ponteiro para o interior de um objeto
Ponteiro pendente
Ponteiro temporário
tmp = realloc(p, bytes), um resultado nulo deixa p disponível para uso contínuo ou liberação de recursos.Possivelmente perdida
Primeiro erro relatado
R
Rastreamento da pilha de chamadas
Rastreamento de origem
--track-origins=yes do Memcheck pode explicar por que uma condição posterior depende de dados não definidos no heap.realloc
realloc substitui um bloco alocado por um objeto do tamanho solicitado, preservando a parte inicial que cabe e podendo alterar seu endereço.Referência alternativa ao mesmo objeto
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
Resumo do heap
Reutilização de buffer
S
size_t
size_t é o tipo inteiro sem sinal usado para tamanhos de objetos e quantidades de bytes de alocação, incluindo o resultado de sizeof.size_t para seu comprimento e sua capacidade, para que possam descrever tamanhos de objetos.sizeof
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.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
realloc encerra o tempo de vida do objeto antigo mesmo quando o endereço retornado parece inalterado.Terminador nulo
U
UndefinedBehaviorSanitizer
-fsanitize=address,undefined para ativar os dois conjuntos compatíveis de verificações.Uso após a liberação
V
Vazamento de memória
- 1Primeiros programas em C
- 2Funções em C
- 3Arranjos e cadeias de caracteres em C
- 4Ponteiros em C
- 5Gerenciamento de memória dinâmica em C
- 6Estruturas de dados em C
- 7Entrada e saída de arquivos em C
- 8Depuração em C
Sobre esta aula
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.