Eu procurei, mas não entendi muito bem esses três conceitos. Quando devo usar a alocação dinâmica (na pilha) e qual é a sua real vantagem? Quais são os problemas de estática e pilha? Eu poderia escrever um aplicativo inteiro sem alocar variáveis na pilha?
Ouvi dizer que outras línguas incorporam um "coletor de lixo" para que você não precise se preocupar com memória. O que o coletor de lixo faz?
O que você poderia fazer manipulando a memória por si mesmo que não poderia fazer usando esse coletor de lixo?
Uma vez alguém me disse que com esta declaração:
int * asafe=new int;
Eu tenho um "ponteiro para um ponteiro". O que isso significa? É diferente de:
asafe=new int;
?
Respostas:
Uma pergunta semelhante foi feita, mas não perguntou sobre estática.
Resumo do que são estáticas, heap e memória de pilha:
Uma variável estática é basicamente uma variável global, mesmo que você não possa acessá-la globalmente. Geralmente, há um endereço para ele que está no próprio executável. Há apenas uma cópia para o programa inteiro. Não importa quantas vezes você entre em uma chamada de função (ou classe) (e em quantos threads!) A variável está se referindo ao mesmo local de memória.
A pilha é um monte de memória que pode ser usada dinamicamente. Se você deseja 4kb para um objeto, o alocador dinâmico examinará sua lista de espaço livre no heap, escolha um pedaço de 4kb e o entregue a você. Geralmente, o alocador de memória dinâmica (malloc, new, etc.) inicia no final da memória e trabalha para trás.
Explicar como uma pilha cresce e diminui está um pouco fora do escopo desta resposta, mas basta dizer que você sempre adiciona e remove apenas do final. As pilhas geralmente começam altas e crescem para endereços mais baixos. Você fica sem memória quando a pilha encontra o alocador dinâmico em algum lugar no meio (mas consulte a memória física e a virtual e a fragmentação). Vários encadeamentos exigirão várias pilhas (o processo geralmente reserva um tamanho mínimo para a pilha).
Quando você deseja usar cada um:
As estáticas / globais são úteis para a memória que você sabe que sempre precisará e que nunca deseja desalocar. (A propósito, ambientes incorporados podem ser considerados como tendo apenas memória estática ... a pilha e a pilha são parte de um espaço de endereço conhecido compartilhado por um terceiro tipo de memória: o código do programa. Os programas geralmente fazem alocação dinâmica a partir de suas memória estática quando eles precisam de coisas como listas vinculadas, mas, independentemente disso, a própria memória estática (o buffer) não é "alocada", mas outros objetos são alocados para fora da memória mantida pelo buffer para esse fim. também não embutidos, e os jogos de console evitam frequentemente os mecanismos de memória dinâmica incorporados, a fim de controlar rigidamente o processo de alocação usando buffers de tamanhos predefinidos para todas as alocações.)
As variáveis de pilha são úteis quando você sabe que, enquanto a função estiver no escopo (na pilha em algum lugar), você desejará que as variáveis permaneçam. As pilhas são boas para variáveis necessárias para o código em que estão localizadas, mas que não são necessárias fora desse código. Eles também são muito bons quando você está acessando um recurso, como um arquivo, e deseja que o recurso desapareça automaticamente quando você deixar esse código.
As alocações de heap (memória alocada dinamicamente) são úteis quando você deseja ser mais flexível que o acima. Freqüentemente, uma função é chamada para responder a um evento (o usuário clica no botão "criar caixa"). A resposta adequada pode exigir a alocação de um novo objeto (um novo objeto Box) que deve permanecer por muito tempo depois que a função é encerrada, para que não possa estar na pilha. Mas você não sabe quantas caixas deseja no início do programa, portanto não pode ser estático.
Coleta de lixo
Ultimamente, ouvi muito sobre o quão bom é o Garbage Collectors, então talvez uma voz um pouco dissidente seja útil.
A coleta de lixo é um mecanismo maravilhoso para quando o desempenho não é um problema enorme. Ouvi dizer que os GCs estão ficando melhores e mais sofisticados, mas o fato é que você pode ser forçado a aceitar uma penalidade de desempenho (dependendo do caso de uso). E se você é preguiçoso, ainda pode não funcionar corretamente. Na melhor das hipóteses, os Garbage Collectors percebem que sua memória se esgota quando percebe que não há mais referências a ela (consulte a contagem de referências) Mas, se você tiver um objeto que se refere a si mesmo (possivelmente se referindo a outro objeto que se refere novamente), a contagem de referências sozinha não indicará que a memória pode ser excluída. Nesse caso, o GC precisa examinar toda a sopa de referência e descobrir se existem ilhas que são apenas referidas por elas mesmas. De antemão, eu acho que essa é uma operação O (n ^ 2), mas seja o que for, pode ficar ruim se você estiver preocupado com o desempenho. (Edit: Martin B ressalta que é O (n) para algoritmos razoavelmente eficientes. Isso ainda é O (n) demais se você estiver preocupado com o desempenho e puder se desalocar em tempo constante sem coleta de lixo.)
Pessoalmente, quando ouço as pessoas dizerem que o C ++ não tem coleta de lixo, minha mente identifica isso como um recurso do C ++, mas provavelmente sou uma minoria. Provavelmente, a coisa mais difícil para as pessoas aprenderem sobre programação em C e C ++ são os indicadores e como lidar corretamente com suas alocações de memória dinâmica. Algumas outras linguagens, como Python, seriam horríveis sem o GC, então acho que tudo se resume ao que você quer de uma linguagem. Se você quer um desempenho confiável, o C ++ sem coleta de lixo é a única coisa que posso pensar neste lado do Fortran. Se você deseja facilidade de uso e rodinhas de treinamento (para evitar colisões sem exigir que você aprenda o gerenciamento de memória "adequado"), escolha algo com um GC. Mesmo que você saiba como gerenciar bem a memória, você economizará tempo e poderá otimizar outro código. Realmente não há mais uma penalidade de desempenho, mas se você realmente precisar de um desempenho confiável (e a capacidade de saber exatamente o que está acontecendo, quando, oculto), então eu continuaria com o C ++. Há uma razão pela qual todos os principais mecanismos de jogos que eu já ouvi falar estão em C ++ (se não C ou assembly). Python, et al são bons para scripts, mas não o principal mecanismo de jogo.
fonte
Obviamente, o seguinte não é totalmente preciso. Tome-o com um grão de sal quando o ler :)
Bem, as três coisas a que você se refere são a duração automática, estática e dinâmica do armazenamento , que tem algo a ver com o tempo de vida dos objetos e quando eles começam a vida.
Duração de armazenamento automático
Você usa a duração automática do armazenamento para dados pequenos e de curta duração , necessários apenas localmente em algum bloco:
A vida útil termina assim que saímos do bloco e inicia assim que o objeto é definido. Eles são o tipo mais simples de duração do armazenamento e são muito mais rápidos do que em particular a duração dinâmica do armazenamento.
Duração de armazenamento estático
Você usa a duração do armazenamento estático para variáveis livres, que podem ser acessadas por qualquer código o tempo todo, se seu escopo permitir esse uso (escopo de namespace) e para variáveis locais que precisam estender sua vida útil pela saída de seu escopo (escopo local) e para variáveis de membro que precisam ser compartilhadas por todos os objetos de sua classe (escopo de classes). A vida útil deles depende do escopo em que estão. Eles podem ter escopo de espaço para nome e escopo local e escopo de classe . O que é verdade sobre os dois é que, assim que a vida começa, a vida termina no final do programa . Aqui estão dois exemplos:
O programa é impresso
ababab
, porquelocalA
não é destruído na saída de seu bloco. Você pode dizer que objetos com escopo local começam a vida útil quando o controle atinge sua definição . PoislocalA
isso acontece quando o corpo da função é inserido. Para objetos no escopo do espaço para nome, o tempo de vida começa na inicialização do programa . O mesmo vale para objetos estáticos do escopo da classe:Como você vê,
classScopeA
não está vinculado a objetos específicos de sua classe, mas à própria classe. O endereço dos três nomes acima é o mesmo e todos denotam o mesmo objeto. Existem regras especiais sobre quando e como os objetos estáticos são inicializados, mas não vamos nos preocupar com isso agora. Isso significa o termo fiasco da ordem de inicialização estática .Duração de armazenamento dinâmico
A última duração do armazenamento é dinâmica. Você o usa se quiser que os objetos morem em outra ilha e queira colocar indicadores em torno deles. Você também os utiliza se seus objetos forem grandes e se desejar criar matrizes de tamanho conhecidas apenas em tempo de execução . Devido a essa flexibilidade, objetos com duração de armazenamento dinâmico são complicados e lentos para gerenciar. Objetos com essa duração dinâmica começam a vida útil quando ocorre uma nova chamada de operador apropriada :
Sua vida útil termina somente quando você chama delete para eles. Se você esquecer disso, esses objetos nunca terminam a vida útil. E os objetos de classe que definem um construtor declarado pelo usuário não terão seus destruidores chamados. Objetos com duração de armazenamento dinâmico requerem manipulação manual de sua vida útil e recursos de memória associados. Existem bibliotecas para facilitar o uso delas. A coleta de lixo explícita para objetos específicos pode ser estabelecida usando um ponteiro inteligente:
Você não precisa se preocupar em chamar delete: o ptr compartilhado faz isso por você, se o último ponteiro que faz referência ao objeto ficar fora do escopo. O próprio ptr compartilhado possui duração de armazenamento automático. Portanto, seu tempo de vida é gerenciado automaticamente, permitindo que você verifique se deve excluir o objeto dinâmico apontado para seu destruidor. Para referência shared_ptr, consulte documentos de aumento: http://www.boost.org/doc/libs/1_37_0/libs/smart_ptr/shared_ptr.htm
fonte
Foi dito elaboradamente, assim como "a resposta curta":
duração da variável estática (classe) = tempo de execução do programa (1)
visibilidade = determinada pelos modificadores de acesso (privado / protegido / público)
duração estática da variável (escopo global) = tempo de execução do programa (1)
visibilidade = a unidade de compilação na qual é instanciada (2)
vida útil da variável de pilha = definida por você (novo para excluir)
visibilidade = definida por você (seja qual for o nome do ponteiro)
visibilidade da variável da pilha = da declaração até que o escopo seja encerrado
tempo de vida = da declaração até que o escopo seja declarado
(1) mais exatamente: da inicialização até a desinicialização da unidade de compilação (ou seja, arquivo C / C ++). A ordem de inicialização das unidades de compilação não é definida pelo padrão.
(2) Cuidado: se você instanciar uma variável estática em um cabeçalho, cada unidade de compilação obtém sua própria cópia.
fonte
Tenho certeza de que um dos pedantes terá uma resposta melhor em breve, mas a principal diferença é velocidade e tamanho.
Pilha
Muito mais rápido para alocar. Isso é feito em O (1), uma vez que é alocado ao configurar o quadro da pilha, para que fique essencialmente livre. A desvantagem é que, se você ficar sem espaço na pilha, estará desossado. Você pode ajustar o tamanho da pilha, mas IIRC tem ~ 2 MB para jogar. Além disso, assim que você sai da função, tudo na pilha é limpo. Portanto, pode ser problemático consultá-lo mais tarde. (Ponteiros para empilhar objetos alocados levam a erros.)
Montão
Dramaticamente mais lento para alocar. Mas você tem GB para brincar e apontar para.
Coletor de lixo
O coletor de lixo é um código que é executado em segundo plano e libera memória. Quando você aloca memória no heap, é muito fácil esquecer de liberá-lo, conhecido como vazamento de memória. Com o tempo, a memória que seu aplicativo consome aumenta e cresce até travar. Ter um coletor de lixo periodicamente liberando a memória que você não precisa mais ajuda a eliminar essa classe de erros. É claro que isso tem um preço, pois o coletor de lixo retarda as coisas.
fonte
O problema com a alocação "estática" é que a alocação é feita no tempo de compilação: você não pode usá-lo para alocar um número variável de dados, cujo número não é conhecido até o tempo de execução.
O problema com a alocação na "pilha" é que a alocação é destruída assim que a sub-rotina que faz a alocação retornar.
Talvez, mas não seja, um aplicativo grande e não trivial, normal (mas os chamados programas "incorporados" podem ser gravados sem a pilha, usando um subconjunto de C ++).
Ele continua assistindo seus dados ("marcar e varrer") para detectar quando seu aplicativo não está mais fazendo referência a eles. Isso é conveniente para o aplicativo, porque ele não precisa desalocar os dados ... mas o coletor de lixo pode ser computacionalmente caro.
Coletores de lixo não são um recurso comum da programação C ++.
Aprenda os mecanismos C ++ para desalocação determinística de memória:
fonte
A alocação de memória da pilha (variáveis de função, variáveis locais) pode ser problemática quando sua pilha é muito "profunda" e você excede a memória disponível para alocações de pilha. O heap é para objetos que precisam ser acessados de vários encadeamentos ou ao longo do ciclo de vida do programa. Você pode escrever um programa inteiro sem usar o heap.
Você pode vazar memória facilmente sem um coletor de lixo, mas também pode determinar quando os objetos e a memória são liberados. Eu me deparei com problemas com Java quando ele executa o GC e tenho um processo em tempo real, porque o GC é um encadeamento exclusivo (nada mais pode ser executado). Portanto, se o desempenho é crítico e você pode garantir que não há objetos vazados, não é muito útil usar um GC. Caso contrário, você só odeia a vida quando seu aplicativo consome memória e você precisa rastrear a origem de um vazamento.
fonte
E se o seu programa não souber antecipadamente quanta memória alocar (portanto, você não pode usar variáveis de pilha). Por exemplo, listas vinculadas, as listas podem crescer sem saber antecipadamente qual é o seu tamanho. Portanto, alocar em um heap faz sentido para uma lista vinculada quando você não está ciente de quantos elementos seriam inseridos nela.
fonte
Uma vantagem do GC em algumas situações é um aborrecimento em outras; a confiança no GC encoraja não pensar muito sobre isso. Em teoria, aguarde até o período 'inativo' ou até que seja absolutamente necessário, quando ele roubará a largura de banda e causará latência de resposta no seu aplicativo.
Mas você não precisa 'não pensar nisso'. Assim como tudo em aplicativos multithread, quando você pode produzir, você pode produzir. Por exemplo, em .Net, é possível solicitar um GC; ao fazer isso, em vez de um GC com execução menos frequente, você pode ter um GC com menor frequência e espalhar a latência associada a essa sobrecarga.
Mas isso derrota a atração principal do GC, que parece ser "encorajada a não ter que pensar muito sobre isso porque é auto-mat-ic".
Se você foi exposto à programação antes de o GC se tornar predominante e se sentir confortável com malloc / free e new / delete, pode ser que você ache o GC um pouco irritante e / ou desconfie (como se pode desconfiar de ' otimização ", que possui um histórico quadriculado.) Muitos aplicativos toleram latência aleatória. Mas para aplicativos que não o fazem, onde a latência aleatória é menos aceitável, uma reação comum é evitar os ambientes de GC e seguir na direção de código puramente não gerenciado (ou, Deus me livre, uma arte que está morrendo há muito tempo, linguagem assembly).
Há um tempo, tive um estudante de verão aqui, um estagiário, garoto esperto, que foi desmamado na GC; ele era tão venerado pelo excesso de GC que, mesmo ao programar em C / C ++ não gerenciado, recusou-se a seguir o modelo malloc / free new / delete porque, citação, "você não deveria fazer isso em uma linguagem de programação moderna". E você sabe? Para aplicativos minúsculos e de execução curta, você pode realmente se safar disso, mas não para aplicativos de alto desempenho.
fonte
Stack é uma memória alocada pelo compilador, sempre que compilamos o programa, no compilador padrão aloca um pouco de memória do sistema operacional (podemos alterar as configurações das configurações do compilador no seu IDE) e o sistema operacional é o que fornece a memória, depende em muitas memórias disponíveis no sistema e muitas outras coisas, e chegar à pilha de memória é alocado quando declaramos uma variável que eles copiam (ref como formais) essas variáveis são pressionadas para empilhar e seguem algumas convenções de nomenclatura por padrão, seu CDECL nos estúdios visuais ex: notação infix: c = a + b; o empilhamento da pilha é feito da direita para a esquerda PUSHING, b para empilhar, operador, a para empilhar e o resultado daqueles i, ec para empilhar. Na notação pré-fixada: = + cab Aqui todas as variáveis são pressionadas para empilhar 1 (da direita para a esquerda) e depois a operação é feita. Essa memória alocada pelo compilador é corrigida. Então, vamos supor que 1 MB de memória seja alocado para o nosso aplicativo, digamos que as variáveis usem 700kb de memória (todas as variáveis locais são empurradas para a pilha, a menos que sejam dinamicamente alocadas), portanto, a memória 324kb restante é alocada para o heap. E essa pilha tem menos tempo de vida útil, quando o escopo da função termina, essas pilhas são limpas.
fonte