Gerenciamento de Memória C

90

Sempre ouvi dizer que em C você realmente precisa observar como administra a memória. E ainda estou começando a aprender C, mas até agora não tive que fazer nenhuma atividade relacionada ao gerenciamento de memória. Sempre imaginei ter que liberar variáveis ​​e fazer todo tipo de coisa feia. Mas não parece ser o caso.

Alguém pode me mostrar (com exemplos de código) um exemplo de quando você teria que fazer algum "gerenciamento de memória"?

The.Anti.9
fonte
Bom lugar para aprender G4G
EsmaeelE

Respostas:

230

Existem dois locais onde as variáveis ​​podem ser colocadas na memória. Quando você cria uma variável como esta:

int  a;
char c;
char d[16];

As variáveis ​​são criadas na " pilha ". As variáveis ​​de pilha são liberadas automaticamente quando saem do escopo (ou seja, quando o código não pode mais alcançá-las). Você pode ouvi-las chamadas de variáveis ​​"automáticas", mas isso saiu de moda.

Muitos exemplos para iniciantes usarão apenas variáveis ​​de pilha.

A pilha é boa porque é automática, mas também tem duas desvantagens: (1) O compilador precisa saber com antecedência o tamanho das variáveis ​​e (b) o espaço da pilha é um tanto limitado. Por exemplo: no Windows, nas configurações padrão do vinculador da Microsoft, a pilha é definida como 1 MB e nem toda ela está disponível para suas variáveis.

Se você não sabe em tempo de compilação o quão grande é o seu array, ou se você precisa de um grande array ou struct, você precisa do "plano B".

O plano B é chamado de " heap ". Normalmente, você pode criar variáveis ​​tão grandes quanto o sistema operacional permitir, mas você tem que fazer isso sozinho. Postagens anteriores mostraram uma maneira de fazer isso, embora existam outras maneiras:

int size;
// ...
// Set size to some value, based on information available at run-time. Then:
// ...
char *p = (char *)malloc(size);

(Observe que as variáveis ​​na pilha não são manipuladas diretamente, mas por meio de ponteiros)

Depois de criar uma variável de heap, o problema é que o compilador não pode dizer quando você terminou com ela, então você perde a liberação automática. É aí que entra a "liberação manual" a que você se referia. Seu código agora é responsável por decidir quando a variável não é mais necessária e liberá-la para que a memória possa ser usada para outros fins. Para o caso acima, com:

free(p);

O que torna essa segunda opção um "negócio desagradável" é que nem sempre é fácil saber quando a variável não é mais necessária. Esquecer de liberar uma variável quando você não precisa dela fará com que seu programa consuma mais memória do que precisa. Essa situação é chamada de "vazamento". A memória "vazada" não pode ser usada para nada até que o programa termine e o sistema operacional recupere todos os seus recursos. Problemas ainda mais desagradáveis ​​são possíveis se você liberar uma variável de heap por engano antes de realmente terminar com ela.

Em C e C ++, você é responsável por limpar suas variáveis ​​de heap como mostrado acima. No entanto, existem linguagens e ambientes como Java e linguagens .NET como C # que usam uma abordagem diferente, onde o heap é limpo por conta própria. Este segundo método, chamado de "coleta de lixo", é muito mais fácil para o desenvolvedor, mas você paga uma penalidade na sobrecarga e no desempenho. É um equilíbrio.

(Eu encostei muitos detalhes para dar uma resposta mais simples, mas espero que mais nivelada)

Euro Micelli
fonte
3
Se você deseja colocar algo na pilha, mas não sabe o quão grande é em tempo de compilação, alloca () pode aumentar o frame da pilha para liberar espaço. Não há freea (), todo o stack frame é exibido quando a função retorna. Usar alloca () para grandes alocações é muito perigoso.
DGentry
1
Talvez você possa adicionar uma ou duas frases sobre a localização da memória de variáveis ​​globais
Michael Käfer
Em C nunca elenco o retorno de malloc()sua causa UB, (char *)malloc(size);consulte stackoverflow.com/questions/605845/…
EsmaeelE
17

Aqui está um exemplo. Suponha que você tenha uma função strdup () que duplica uma string:

char *strdup(char *src)
{
    char * dest;
    dest = malloc(strlen(src) + 1);
    if (dest == NULL)
        abort();
    strcpy(dest, src);
    return dest;
}

E você chama assim:

main()
{
    char *s;
    s = strdup("hello");
    printf("%s\n", s);
    s = strdup("world");
    printf("%s\n", s);
}

Você pode ver que o programa funciona, mas alocou memória (via malloc) sem liberá-la. Você perdeu o ponteiro para o primeiro bloco de memória ao chamar strdup pela segunda vez.

Isso não é grande coisa para essa pequena quantidade de memória, mas considere o caso:

for (i = 0; i < 1000000000; ++i)  /* billion times */
    s = strdup("hello world");    /* 11 bytes */

Você acabou de usar 11 GB de memória (possivelmente mais, dependendo do seu gerenciador de memória) e, se não travou, o processo provavelmente está lento.

Para corrigir, você precisa chamar free () para tudo o que é obtido com malloc () depois de terminar de usá-lo:

s = strdup("hello");
free(s);  /* now not leaking memory! */
s = strdup("world");
...

Espero que este exemplo ajude!

Mark Harrison
fonte
Eu gosto mais dessa resposta. Mas eu tenho uma pequena pergunta lateral. Eu esperaria que algo assim fosse resolvido com bibliotecas, não existe uma biblioteca que imitaria de perto os tipos de dados básicos e adicionaria recursos de liberação de memória a eles para que, quando as variáveis ​​forem usadas, elas também sejam liberadas automaticamente?
Lorenzo
Nenhum que faça parte do padrão. Se você for para C ++, obterá strings e contêineres que fazem gerenciamento automático de memória.
Mark Harrison
Entendo, então existem algumas bibliotecas de terceiros? Você poderia, por favor, nomeá-los?
Lorenzo
9

Você precisa fazer "gerenciamento de memória" quando quiser usar a memória no heap em vez da pilha. Se você não souber o tamanho de um array até o tempo de execução, terá que usar o heap. Por exemplo, você pode querer armazenar algo em uma string, mas não sabe quão grande será seu conteúdo até que o programa seja executado. Nesse caso, você escreveria algo assim:

 char *string = malloc(stringlength); // stringlength is the number of bytes to allocate

 // Do something with the string...

 free(string); // Free the allocated memory
Jeremy Ruten
fonte
5

Acho que a maneira mais concisa de responder à pergunta é considerar o papel do ponteiro em C. O ponteiro é um mecanismo leve, mas poderoso, que lhe dá imensa liberdade ao custo de uma imensa capacidade de atirar no próprio pé.

Em C, a responsabilidade de garantir que seus indicadores apontem para a memória que você possui é sua e somente sua. Isso requer uma abordagem organizada e disciplinada, a menos que você ignore as dicas, o que torna difícil escrever um C.

As respostas postadas até agora se concentram em alocações de variáveis ​​automáticas (pilha) e heap. Usar a alocação de pilha cria uma memória conveniente e gerenciada automaticamente, mas em algumas circunstâncias (grandes buffers, algoritmos recursivos) pode levar ao terrível problema de estouro de pilha. Saber exatamente quanta memória você pode alocar na pilha depende muito do sistema. Em alguns cenários integrados, algumas dezenas de bytes podem ser o seu limite; em alguns cenários de desktop, você pode usar megabytes com segurança.

A alocação de heap é menos inerente ao idioma. É basicamente um conjunto de chamadas de biblioteca que garante a propriedade de um bloco de memória de determinado tamanho até que você esteja pronto para devolvê-lo ('liberá-lo'). Parece simples, mas está associado a uma dor incalculável do programador. Os problemas são simples (liberar a mesma memória duas vezes ou não [vazamentos de memória], não alocar memória suficiente [estouro de buffer], etc), mas difíceis de evitar e depurar. Uma abordagem altamente disciplinada é absolutamente obrigatória na prática, mas é claro que a linguagem não a exige.

Eu gostaria de mencionar outro tipo de alocação de memória que foi ignorado por outros posts. É possível alocar variáveis ​​estaticamente, declarando-as fora de qualquer função. Acho que, em geral, esse tipo de alocação tem uma má reputação porque é usado por variáveis ​​globais. No entanto, não há nada que diga que a única maneira de usar a memória alocada dessa forma é como uma variável global indisciplinada em uma bagunça de código espaguete. O método de alocação estática pode ser usado simplesmente para evitar algumas das armadilhas do heap e métodos de alocação automática. Alguns programadores C ficam surpresos ao saber que grandes e sofisticados programas de jogos e embarcados em C foram construídos sem o uso de alocação de heap.

Bill Forster
fonte
4

Existem algumas ótimas respostas aqui sobre como alocar e liberar memória e, na minha opinião, o lado mais desafiador de usar C é garantir que a única memória que você usa seja a que você alocou - se isso não for feito corretamente, o que você termina é o primo deste site - um estouro de buffer - e você pode estar sobrescrevendo a memória que está sendo usada por outro aplicativo, com resultados muito imprevisíveis.

Um exemplo:

int main() {
    char* myString = (char*)malloc(5*sizeof(char));
    myString = "abcd";
}

Neste ponto, você alocou 5 bytes para myString e o preencheu com "abcd \ 0" (strings terminam em nulo - \ 0). Se sua alocação de string foi

myString = "abcde";

Você estaria atribuindo "abcde" nos 5 bytes alocados para o seu programa, e o caractere nulo final seria colocado no final disso - uma parte da memória que não foi alocada para seu uso e pode ser grátis, mas também pode estar sendo usado por outro aplicativo - Esta é a parte crítica do gerenciamento de memória, onde um erro terá consequências imprevisíveis (e às vezes irrepetíveis).

Chris BC
fonte
Aqui você aloca 5 bytes. Solte-o atribuindo um ponteiro. Qualquer tentativa de liberar esse ponteiro leva a um comportamento indefinido. Nota As C-Strings não sobrecarregam o operador =, não há cópia.
Martin York
Porém, realmente depende do malloc que você está usando. Muitos operadores malloc se alinham a 8 bytes. Portanto, se este malloc estiver usando um sistema de cabeçalho / rodapé, malloc reservará 5 + 4 * 2 (4 bytes para cabeçalho e rodapé). Isso seria 13 bytes, e malloc forneceria apenas 3 bytes extras para alinhamento. Não estou dizendo que seja uma boa ideia usar isso, porque só vai funcionar em sistemas cujo malloc funciona assim, mas é pelo menos importante saber por que fazer algo errado pode funcionar.
kodai
Loki: Eu editei a resposta para usar em strcpy()vez de =; Presumo que seja essa a intenção de Chris BC.
echristopherson
Acredito que a proteção de memória de hardware de plataformas modernas evita que os processos do espaço do usuário sobrescrevam os espaços de endereço de outros processos; você obteria uma falha de segmentação em vez disso. Mas isso não faz parte do C em si.
echristopherson
4

Uma coisa a lembrar é sempre inicializar seus ponteiros para NULL, já que um ponteiro não inicializado pode conter um endereço de memória válido pseudo-aleatório que pode fazer com que os erros de ponteiro ocorram silenciosamente. Ao forçar um ponteiro a ser inicializado com NULL, você sempre pode detectar se está usando esse ponteiro sem inicializá-lo. O motivo é que os sistemas operacionais "conectam" o endereço virtual 0x00000000 a exceções de proteção geral para interceptar o uso de ponteiro nulo.

Hernán
fonte
2

Além disso, você pode querer usar a alocação de memória dinâmica quando precisar definir uma grande matriz, digamos int [10000]. Você não pode simplesmente colocá-lo na pilha porque então, hm ... você terá um estouro de pilha.

Outro bom exemplo seria a implementação de uma estrutura de dados, digamos, lista vinculada ou árvore binária. Não tenho um código de exemplo para colar aqui, mas você pode pesquisar no Google facilmente.

Sarja
fonte
2

(Estou escrevendo porque sinto que as respostas até agora não são muito corretas.)

O motivo pelo qual vale a pena mencionar o gerenciamento de memória é quando você tem um problema / solução que exige a criação de estruturas complexas. (Se seus programas travarem se você alocar muito espaço na pilha de uma vez, isso é um bug.) Normalmente, a primeira estrutura de dados que você precisa aprender é algum tipo de lista . Aqui está um único link, no topo da minha cabeça:

typedef struct listelem { struct listelem *next; void *data;} listelem;

listelem * create(void * data)
{
   listelem *p = calloc(1, sizeof(listelem));
   if(p) p->data = data;
   return p;
}

listelem * delete(listelem * p)
{
   listelem next = p->next;
   free(p);
   return next;
}

void deleteall(listelem * p)
{
  while(p) p = delete(p);
}

void foreach(listelem * p, void (*fun)(void *data) )
{
  for( ; p != NULL; p = p->next) fun(p->data);
}

listelem * merge(listelem *p, listelem *q)
{
  while(p != NULL && p->next != NULL) p = p->next;
  if(p) {
    p->next = q;
    return p;
  } else
    return q;
}

Naturalmente, você gostaria de algumas outras funções, mas basicamente, é para isso que você precisa do gerenciamento de memória. Devo salientar que existem vários truques que são possíveis com o gerenciamento de memória "manual", por exemplo,

  • Usando o fato de que malloc é garantido (pelo padrão da linguagem) para retornar um ponteiro divisível por 4,
  • alocar espaço extra para algum propósito sinistro seu,
  • criando pool de memória s ..

Obtenha um bom depurador ... Boa sorte!

Anders Eurenius
fonte
Aprender estruturas de dados é a próxima etapa chave na compreensão do gerenciamento de memória. Aprender os algoritmos para executar adequadamente essas estruturas irá mostrar os métodos apropriados para superar esses golpes. É por isso que você encontrará estruturas de dados e algoritmos ensinados nos mesmos cursos.
aj.toulan
0

@ Euro Micelli

Um ponto negativo a adicionar é que os ponteiros para a pilha não são mais válidos quando a função retorna, portanto, você não pode retornar um ponteiro para uma variável da pilha de uma função. Este é um erro comum e um dos principais motivos pelos quais você não consegue sobreviver apenas com variáveis ​​de pilha. Se sua função precisar retornar um ponteiro, você terá que malloc e lidar com o gerenciamento de memória.

Jonathan Branam
fonte
0

@ Ted Percival :
... você não precisa converter o valor de retorno de malloc ().

Você está correto, é claro. Acredito que sempre foi verdade, embora eu não tenha uma cópia de K&R para verificar.

Eu não gosto muito das conversões implícitas em C, então eu tendo a usar conversões para tornar a "mágica" mais visível. Às vezes ajuda a legibilidade, às vezes não e às vezes faz com que um bug silencioso seja detectado pelo compilador. Ainda assim, não tenho uma opinião forte sobre isso, de uma forma ou de outra.

Isso é especialmente provável se o seu compilador entender comentários no estilo C ++.

Sim ... você me pegou lá. Eu passo muito mais tempo em C ++ do que em C. Obrigado por notar isso.

Euro Micelli
fonte
@echristopherson, obrigado. Você está certo - mas observe que esta pergunta / resposta foi de agosto de 2008, antes mesmo de o Stack Overflow ser beta público. Naquela época, ainda estávamos descobrindo como o site deveria funcionar. O formato desta pergunta / resposta não deve ser necessariamente visto como um modelo de como usar o OS. Obrigado!
Euro Micelli
Ah, obrigado por apontar isso - eu não sabia que esse aspecto do site ainda estava mudando então.
echristopherson
0

Em C, você realmente tem duas opções diferentes. Um, você pode deixar o sistema gerenciar a memória para você. Alternativamente, você pode fazer isso sozinho. Geralmente, você deseja manter o primeiro tanto quanto possível. No entanto, a memória gerenciada automaticamente em C é extremamente limitada e você precisará gerenciar manualmente a memória em muitos casos, como:

uma. Você quer que a variável sobreviva às funções e não quer ter uma variável global. ex:

struct pair {
   int val;
   par de estrutura * próximo;
}

par de estrutura * par_novo (int val) {
   par de estrutura * np = malloc (sizeof (par de estrutura));
   np-> val = val;
   np-> próximo = NULL;
   return np;
}

b. você deseja ter memória alocada dinamicamente. O exemplo mais comum é a matriz sem comprimento fixo:

int * my_special_array;
my_special_array = malloc (sizeof (int) * number_of_element);
para (i = 0; i

c. Você quer fazer algo REALMENTE sujo. Por exemplo, eu gostaria que uma estrutura representasse muitos tipos de dados e não gosto de união (união parece tãããão confusa):

struct data { int data_type; long data_in_mem; }; struct animal {/ * algo * /}; struct person {/ * alguma outra coisa * /}; struct animal * read_animal (); struct person * read_person (); / * In main * / amostra de dados de estrutura; sampe.data_type = input_type; switch (input_type) { caso DATA_PERSON: sample.data_in_mem = read_person (); pausa; case DATA_ANIMAL: sample.data_in_mem = read_animal (); padrão: printf ("Oh hoh! Eu te aviso, isso de novo e irei seg avariar seu SO"); }

Veja, um valor longo é suficiente para conter QUALQUER COISA. Apenas lembre-se de liberá-lo ou você se arrependerá. Este é um dos meus truques favoritos para se divertir em C: D.

No entanto, geralmente, você gostaria de ficar longe de seus truques favoritos (T___T). Você IRÁ quebrar seu sistema operacional, mais cedo ou mais tarde, se usá-los com muita frequência. Contanto que você não use * alloc e free, é seguro dizer que você ainda é virgem e que o código ainda parece bom.

magia
fonte
"Veja, um valor longo é suficiente para conter QUALQUER COISA" -: / do que você está falando, na maioria dos sistemas um valor longo é 4 bytes, exatamente o mesmo que um int. A única razão pela qual os ponteiros cabem aqui é porque o tamanho de long é o mesmo que o tamanho do ponteiro. No entanto, você realmente deveria usar void *.
Score_Em
-2

Certo. Se você criar um objeto que existe fora do escopo em que você o usa. Aqui está um exemplo inventado (tenha em mente que minha sintaxe estará errada; meu C está enferrujado, mas este exemplo ainda ilustrará o conceito):

class MyClass
{
   SomeOtherClass *myObject;

   public MyClass()
   {
      //The object is created when the class is constructed
      myObject = (SomeOtherClass*)malloc(sizeof(myObject));
   }

   public ~MyClass()
   {
      //The class is destructed
      //If you don't free the object here, you leak memory
      free(myObject);
   }

   public void SomeMemberFunction()
   {
      //Some use of the object
      myObject->SomeOperation();
   }


};

Neste exemplo, estou usando um objeto do tipo SomeOtherClass durante o tempo de vida de MyClass. O objeto SomeOtherClass é usado em várias funções, portanto, aloquei dinamicamente a memória: o objeto SomeOtherClass é criado quando MyClass é criado, usado várias vezes durante a vida útil do objeto e, em seguida, liberado quando MyClass é liberado.

Obviamente, se fosse um código real, não haveria razão (além do possível consumo de memória da pilha) para criar myObject dessa forma, mas esse tipo de criação / destruição de objeto torna-se útil quando você tem muitos objetos e deseja controlar com precisão quando eles são criados e destruídos (para que seu aplicativo não absorva 1 GB de RAM por toda a sua vida, por exemplo), e em um ambiente em janela, isso é praticamente obrigatório, como objetos que você cria (botões, por exemplo) , precisam existir bem fora do escopo de qualquer função específica (ou mesmo da classe).

O Smurf
fonte
1
Heh, sim, é C ++, não é? Incrível como demorou cinco meses para alguém me questionar sobre isso.
TheSmurf de