Pergunta

Sempre ouvi dizer que em C você precisa realmente observar como gerencia 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 este 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"?

Foi útil?

Solução

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 no "pilha".Variáveis ​​de pilha são liberadas automaticamente quando saem do escopo (ou seja, quando o código não consegue mais alcançá-las).Você pode ouvi-las serem 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 antecipadamente 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 tamanho do seu array, ou se precisa de um array ou estrutura grande, você precisa do "plano B".

O Plano B é chamado de "amontoar".Geralmente você pode criar variáveis ​​tão grandes quanto o sistema operacional permitir, mas você terá 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 ​​no heap não são manipuladas diretamente, mas por meio de ponteiros)

Depois de criar uma variável heap, o problema é que o compilador não consegue saber quando você terminou de usá-la, 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 esta 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.Esta 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 heap por engano antes você realmente terminou com isso.

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 em custos indiretos e desempenho.É um equilíbrio.

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

Outras dicas

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 você alocou memória (via malloc) sem liberá-la.Você perdeu o ponteiro para o primeiro bloco de memória quando chamou strdup pela segunda vez.

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

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

Você já usou 11 GB de memória (possivelmente mais, dependendo do seu gerenciador de memória) e se não travou, seu processo provavelmente está rodando bem devagar.

Para corrigir, você precisa chamar free() para tudo o que for 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!

Você precisa fazer o "gerenciamento de memória" quando quiser usar a memória no heap em vez da pilha.Se você não sabe o tamanho de um array até o tempo de execução, será necessário usar o heap.Por exemplo, você pode querer armazenar algo em uma string, mas não sabe qual será o tamanho do 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

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 dar um tiro no próprio pé.

Em C, a responsabilidade de garantir que seus ponteiros apontem para a memória que você possui é sua e somente sua.Isso requer uma abordagem organizada e disciplinada, a menos que você abandone os ponteiros, o que torna difícil escrever C eficaz.

As respostas postadas até o momento concentram-se nas alocações automáticas (pilha) e de variáveis ​​​​heap.O uso da alocação de pilha proporciona memória conveniente e gerenciada automaticamente, mas em algumas circunstâncias (buffers grandes, 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 incorporados, 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 à linguagem.É basicamente um conjunto de chamadas de biblioteca que concede a você 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 dos programadores.Os problemas são simples (liberar a mesma memória duas vezes ou não liberar [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 na verdade não exige isso.

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 maneira é como uma variável global indisciplinada em uma confusão de código espaguete.O método de alocação estática pode ser usado simplesmente para evitar algumas das armadilhas dos métodos de heap e de alocação automática.Alguns programadores C ficam surpresos ao saber que programas grandes e sofisticados de jogos e incorporados em C foram construídos sem nenhum uso de alocação de heap.

Existem ótimas respostas aqui sobre como alocar e liberar memória e, na minha opinião, o lado mais desafiador do uso de C é garantir que a única memória que você usa seja a memória que você alocou - se isso não for feito corretamente, o que você terminará está acontecendo com o primo deste site - um buffer overflow - 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 preencheu-o com "abcd\0" (strings terminam em nulo - \0).Se sua alocação de string foi

myString = "abcde";

Você atribuiria "abcde" aos 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 poderia ser gratuito, 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).

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

Além disso, você pode querer usar a alocação dinâmica de memória quando precisar definir um array enorme, digamos int[10000].Você não pode simplesmente empilhá-lo porque então, hm...você obterá 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 pesquisá-lo facilmente no Google.

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

A razão pela 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 só vez, isso é um bug.) Normalmente, a primeira estrutura de dados que você precisará aprender é algum tipo de lista.Aqui está um único vinculado, em cima 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 possíveis com o gerenciamento de memória "manual", por exemplo,

  • Usando o fato de que maloc é garantido (pelo padrão da linguagem) retornar um ponteiro divisível por 4,
  • alocando espaço extra para algum propósito sinistro de sua autoria,
  • criando conjunto de memóriaé..

Obtenha um bom depurador ... Boa sorte!

@Euro Micelli

Um ponto negativo a acrescentar é 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 a partir 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 fazer malloc e lidar com o gerenciamento de memória.

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

Você está correto, é claro.Acredito que isso sempre foi verdade, embora não tenha uma cópia do K&R checar.

Não gosto de muitas conversões implícitas em C, então costumo usar conversões para tornar a "mágica" mais visível.Às vezes ajuda na 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 compreender comentários no estilo C++.

Sim...você me pegou lá.Passo muito mais tempo em C++ do que em C.Obrigado por perceber isso.

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

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

struct pair{
   int val;
   struct pair *next;
}

struct pair* new_pair(int val){
   struct pair* np = malloc(sizeof(struct pair));
   np->val = val;
   np->next = NULL;
   return np;
}

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

int *my_special_array;
my_special_array = malloc(sizeof(int) * number_of_element);
for(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 (a união parece muuuito confusa):

struct data{ int data_type; long data_in_mem; }; struct animal{/*something*/}; struct person{/*some other thing*/}; struct animal* read_animal(); struct person* read_person(); /*In main*/ struct data sample; sampe.data_type = input_type; switch(input_type){ case DATA_PERSON: sample.data_in_mem = read_person(); break; case DATA_ANIMAL: sample.data_in_mem = read_animal(); default: printf("Oh hoh! I warn you, that again and I will seg fault your OS"); }

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 me 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á-lo 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.

Claro.Se você criar um objeto que existe fora do escopo em que você o utiliza.Aqui está um exemplo inventado (tenha em mente que minha sintaxe estará desativada;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 diversas funções, então aloquei a memória dinamicamente:o objeto SomeOtherClass é criado quando MyClass é criado, usado várias vezes ao longo da vida do objeto e, em seguida, liberado quando MyClass é liberado.

Obviamente, se este 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 maneira, mas esse tipo de criação/destruição de objetos se torna ú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 consuma 1 GB de RAM durante toda a sua vida útil, por exemplo) e em um ambiente de 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 de classe).

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top