Pilha, estática e heap em C ++


160

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;

?


Houve uma pergunta muito semelhante feita há algum tempo: O que e onde estão a pilha e a pilha? Existem algumas respostas realmente boas para essa pergunta que devem lançar alguma luz sobre a sua.
Scott Saad

Respostas:


223

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.


Não é realmente relevante para a pergunta original (ou muito, na verdade), mas você tem os locais da pilha e a pilha para trás. Normalmente , a pilha cresce para baixo e a pilha crescer (apesar de um monte na verdade não "crescer", por isso esta é uma enorme simplificação) ...
P Daddy

não acho que essa pergunta seja semelhante ou mesmo duplicada da outra. este é especificamente sobre C ++ e o que ele quis dizer é quase certamente as três durações de armazenamento existentes em C ++. Você pode ter um objeto dinâmico alocado na memória estática, por exemplo, sobrecarregar op novo.
Johannes Schaub - litb 5/01/09

7
Seu tratamento depreciativo da coleta de lixo foi um pouco menos que útil.
P Daddy

9
Atualmente, a coleta de lixo é hoje melhor do que a liberação manual de memória, porque ocorre quando há pouco trabalho a ser feito, em vez de liberar memória que pode acontecer exatamente quando o desempenho pode ser usado de outra forma.
Georg Schölly 11/02/09

3
Apenas um pequeno comentário - a coleta de lixo não tem complexidade O (n ^ 2) (isso seria realmente desastroso para o desempenho). O tempo gasto para um ciclo de coleta de lixo é proporcional ao tamanho da pilha - consulte hpl.hp.com/personal/Hans_Boehm/gc/complexity.html .
Martin B

54

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:

if(some condition) {
    int a[3]; // array a has automatic storage duration
    fill_it(a);
    print_it(a);
}

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:

// static storage duration. in global namespace scope
string globalA; 
int main() {
    foo();
    foo();
}

void foo() {
    // static storage duration. in local scope
    static string localA;
    localA += "ab"
    cout << localA;
}

O programa é impresso ababab, porque localAnã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 . Pois localAisso 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:

class A {
    static string classScopeA;
};

string A::classScopeA;

A a, b; &a.classScopeA == &b.classScopeA == &A::classScopeA;

Como você vê, classScopeAnã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 :

int main() {
    // the object that s points to has dynamic storage 
    // duration
    string *s = new string;
    // pass a pointer pointing to the object around. 
    // the object itself isn't touched
    foo(s);
    delete s;
}

void foo(string *s) {
    cout << s->size();
}

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:

int main() {
    shared_ptr<string> s(new string);
    foo(s);
}

void foo(shared_ptr<string> s) {
    cout << s->size();
}

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


39

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.


5

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.


3

Quais são os problemas de estática e pilha?

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.

Eu poderia escrever um aplicativo inteiro sem alocar variáveis ​​na pilha?

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 ++).

O coletor de lixo faz?

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 ++.

O que você poderia fazer manipulando a memória por si mesmo que não poderia fazer usando esse coletor de lixo?

Aprenda os mecanismos C ++ para desalocação determinística de memória:

  • 'estático': nunca desalocado
  • 'stack': assim que a variável "sair do escopo"
  • 'heap': quando o ponteiro é excluído (excluído explicitamente pelo aplicativo ou excluído implicitamente em alguma sub-rotina)

1

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.


1

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.


0

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.


0

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.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.