Pilha adequada e uso de heap em C ++?


122

Eu tenho programado por um tempo, mas tem sido principalmente Java e C #. Na verdade, nunca tive que gerenciar minha memória sozinha. Recentemente, comecei a programar em C ++ e estou um pouco confuso sobre quando devo armazenar coisas na pilha e quando armazená-las na pilha.

Meu entendimento é que as variáveis ​​que são acessadas com muita frequência devem ser armazenadas na pilha e nos objetos, variáveis ​​raramente usadas e grandes estruturas de dados devem ser armazenadas na pilha. Isso está correto ou estou incorreto?


Respostas:


242

Não, a diferença entre pilha e pilha não é desempenho. É vida útil: qualquer variável local dentro de uma função (qualquer coisa que você não faça malloc () ou nova) fica na pilha. Ele desaparece quando você retorna da função. Se você deseja que algo viva mais do que a função que o declarou, você deve alocá-lo no heap.

class Thingy;

Thingy* foo( ) 
{
  int a; // this int lives on the stack
  Thingy B; // this thingy lives on the stack and will be deleted when we return from foo
  Thingy *pointerToB = &B; // this points to an address on the stack
  Thingy *pointerToC = new Thingy(); // this makes a Thingy on the heap.
                                     // pointerToC contains its address.

  // this is safe: C lives on the heap and outlives foo().
  // Whoever you pass this to must remember to delete it!
  return pointerToC;

  // this is NOT SAFE: B lives on the stack and will be deleted when foo() returns. 
  // whoever uses this returned pointer will probably cause a crash!
  return pointerToB;
}

Para uma compreensão mais clara do que é a pilha, chegue a ela do outro lado - em vez de tentar entender o que a pilha faz em termos de linguagem de alto nível, procure "pilha de chamadas" e "convenção de chamada" e veja o que a máquina realmente funciona quando você chama uma função. A memória do computador é apenas uma série de endereços; "heap" e "stack" são invenções do compilador.


7
Seria seguro acrescentar que informações de tamanhos variados geralmente ficam acumuladas. As únicas exceções que conheço são os VLAs no C99 (que tem suporte limitado) e a função alloca () que geralmente é mal compreendida até pelos programadores C.
1111 Dan Olson

10
Boa explicação, embora em um cenário multithread com alocações e / ou desalocações frequentes, o heap é um ponto de discórdia, afetando o desempenho. Ainda assim, o escopo é quase sempre o fator decisivo.
Peterchen

18
Claro, o novo / malloc () é uma operação lenta, e é mais provável que a pilha esteja no dcache do que uma linha de heap arbitrária. Essas são considerações reais, mas geralmente secundárias à questão da vida útil.
Crashworks

1
É verdade "a memória do computador é apenas uma série de endereços;" heap "e" stack "são invenções da compilação" ?? Eu li em muitos lugares que a pilha é uma região especial da memória do nosso computador.
Vineeth Chitteti

2
@kai Essa é uma maneira de visualizá-lo, mas não é necessariamente verdade fisicamente falando. O sistema operacional é responsável por alocar a pilha e a pilha de um aplicativo. O compilador também é responsável, mas depende principalmente do sistema operacional para fazer isso. A pilha é limitada e a pilha não. Isso se deve à maneira como o sistema operacional lida com a classificação desses endereços de memória em algo mais estruturado para que vários aplicativos possam ser executados no mesmo sistema. Heap e stack não são os únicos, mas geralmente são os dois únicos com os quais a maioria dos desenvolvedores está preocupada.
tsturzl 12/02

42

Eu diria:

Armazene-o na pilha, se puder.

Guarde-o na pilha, se precisar.

Portanto, prefira a pilha ao heap. Algumas razões possíveis pelas quais você não pode armazenar algo na pilha são:

  • É muito grande - em programas multithread no sistema operacional de 32 bits, a pilha tem um tamanho pequeno e fixo (no momento da criação do encadeamento) (normalmente apenas alguns megas), para que você possa criar muitos encadeamentos sem esgotar o endereço Para programas de 64 bits ou programas de thread único (Linux de qualquer maneira), esse não é um problema grave.No Linux de 32 bits, os programas de thread único geralmente usam pilhas dinâmicas que podem continuar crescendo até atingir o topo da pilha.
  • Você precisa acessá-lo fora do escopo do quadro de pilha original - esse é realmente o principal motivo.

É possível, com compiladores sensíveis, alocar objetos de tamanho não fixo na pilha (geralmente matrizes cujo tamanho não é conhecido no tempo de compilação).


1
Qualquer coisa além de alguns KB é geralmente melhor colocada na pilha. Não sei detalhes específicos, mas não me lembro de ter trabalhado com uma pilha "alguns megas".
Dan Olson

2
Isso é algo que eu não diria respeito a um usuário no começo. Para o usuário, vetores e listas parecem estar alocados na pilha, mesmo que esse STL armazene o conteúdo no heap. A pergunta parecia mais na linha de decisão de quando chamar explicitamente novo / excluir.
David Rodríguez - dribeas 01/03

1
Dan: Eu coloquei 2 shows (Sim, G como no GIGS) na pilha no Linux de 32 bits. Os limites de pilha dependem do SO.
31909 Mr.Ree

6
mrree: A pilha do Nintendo DS tem 16 kilobytes. Alguns limites de pilha dependem do hardware.
Ant

Ant: Todas as pilhas dependem do hardware, do SO e também do compilador.
Viliami

24

É mais sutil do que as outras respostas sugerem. Não há uma divisão absoluta entre os dados na pilha e os dados no heap com base em como você os declara. Por exemplo:

std::vector<int> v(10);

No corpo de uma função, que declara um vector(array dinâmico) de dez números inteiros na pilha. Mas o armazenamento gerenciado pelovector não está na pilha.

Ah, mas (as outras respostas sugerem) o tempo de vida desse armazenamento é limitado pelo tempo de vida do vectormesmo, que aqui é baseado em pilha, portanto, não faz diferença como ele é implementado - só podemos tratá-lo como um objeto baseado em pilha com semântica de valores.

Não tão. Suponha que a função fosse:

void GetSomeNumbers(std::vector<int> &result)
{
    std::vector<int> v(10);

    // fill v with numbers

    result.swap(v);
}

Portanto, qualquer coisa com uma swapfunção (e qualquer tipo de valor complexo deve ter uma) pode servir como um tipo de referência recuperável para alguns dados de heap, em um sistema que garante um único proprietário desses dados.

Portanto, a abordagem moderna do C ++ é nunca armazenar o endereço de dados de heap em variáveis ​​de ponteiro local simples. Todas as alocações de heap devem estar ocultas dentro das classes.

Se você fizer isso, poderá pensar em todas as variáveis ​​em seu programa como se fossem tipos simples de valor e esquecer completamente o heap (exceto ao escrever uma nova classe de wrapper semelhante a valor para alguns dados de heap, o que deve ser incomum) .

Você apenas precisa reter um conhecimento especial para ajudá-lo a otimizar: sempre que possível, em vez de atribuir uma variável a outra como esta:

a = b;

troque-os assim:

a.swap(b);

porque é muito mais rápido e não gera exceções. O único requisito é que você não precise bcontinuar com o mesmo valor (isso vai gerara o valor, que seria descartado a = b).

A desvantagem é que essa abordagem obriga a retornar valores de funções por meio de parâmetros de saída em vez do valor de retorno real. Mas eles estão corrigindo isso no C ++ 0x com referências rvalue .

Nas situações mais complicadas de todas, você levaria essa idéia ao extremo geral e usaria uma classe de ponteiro inteligente como a shared_ptrque já está em tr1. (Embora eu argumente que, se você precisar, pode ter se mudado para o ponto ideal de aplicabilidade do Standard C ++.)


6

Você também armazenaria um item no heap se ele precisar ser usado fora do escopo da função na qual ele é criado. Um idioma usado com objetos de pilha é chamado RAII - isso envolve o uso do objeto baseado em pilha como um wrapper para um recurso. Quando o objeto é destruído, o recurso é limpo. Os objetos baseados em pilha são mais fáceis de controlar quando você pode lançar exceções - você não precisa se preocupar em excluir um objeto baseado em heap em um manipulador de exceções. É por isso que os ponteiros brutos normalmente não são usados ​​no C ++ moderno; você usaria um ponteiro inteligente, que pode ser um wrapper baseado em pilha para um ponteiro bruto em um objeto baseado em heap.


5

Para adicionar às outras respostas, também pode ser sobre desempenho, pelo menos um pouco. Não que você deva se preocupar com isso, a menos que seja relevante para você, mas:

A alocação na pilha requer a localização de um rastreamento de um bloco de memória, que não é uma operação de tempo constante (e leva alguns ciclos e sobrecarga). Isso pode ficar mais lento à medida que a memória se fragmenta e / ou você está quase usando 100% do seu espaço de endereço. Por outro lado, as alocações de pilha são operações de tempo constante, basicamente "livres".

Outra coisa a considerar (novamente, realmente importante apenas se houver um problema) é que geralmente o tamanho da pilha é fixo e pode ser muito menor que o tamanho da pilha. Portanto, se você estiver alocando objetos grandes ou muitos objetos pequenos, provavelmente desejará usar o heap; se você ficar sem espaço na pilha, o tempo de execução lançará a exceção do titular do site. Geralmente não é grande coisa, mas outra coisa a considerar.


A pilha e a pilha são memória virtual paginada. O tempo de pesquisa da pilha é incrivelmente rápido comparado ao necessário para mapear na nova memória. No Linux de 32 bits, posso colocar> 2gig na minha pilha. Em Macs, acho que a pilha é limitada a 65Meg.
31909 Mr.Ree

3

A pilha é mais eficiente e mais fácil de gerenciar dados no escopo.

Mas o heap deve ser usado para algo maior que alguns KB (é fácil em C ++, basta criar um boost::scoped_ptrna pilha para manter um ponteiro na memória alocada).

Considere um algoritmo recursivo que continua chamando a si próprio. É muito difícil limitar e / ou adivinhar o uso total da pilha! Enquanto na pilha, o alocador ( malloc()ou new) pode indicar falta de memória retornando NULLou throwing.

Fonte : Kernel Linux cuja pilha não é maior que 8 KB!


Para referência de outros leitores: (A) O "deveria" aqui é puramente a opinião pessoal do usuário, tirada da melhor das hipóteses 1 citação e 1 cenário que é improvável que muitos usuários encontrem (recursão). Além disso, (B) a biblioteca padrão fornece std::unique_ptr, que deve ser preferida a qualquer biblioteca externa como o Boost (embora isso alimente as coisas com o padrão ao longo do tempo).
underscore_d


1

A escolha entre alocar na pilha ou na pilha é aquela que é feita para você, dependendo de como sua variável é alocada. Se você alocar algo dinamicamente, usando uma chamada "nova", estará alocando a partir do heap. Se você alocar algo como variável global ou como parâmetro em uma função, ele será alocado na pilha.


4
Eu suspeito que ele estava perguntando quando colocar as coisas na pilha, não como.
9788 Steve Rowe #

0

Na minha opinião, existem dois fatores decisivos

1) Scope of variable
2) Performance.

Eu preferiria usar a pilha na maioria dos casos, mas se você precisar acessar variáveis ​​fora do escopo, poderá usar heap.

Para aprimorar o desempenho ao usar heaps, você também pode usar a funcionalidade para criar um bloco de heap e isso pode ajudar a obter desempenho em vez de alocar cada variável em um local de memória diferente.


0

provavelmente isso foi respondido muito bem. Gostaria de apontar para a série de artigos abaixo para entender melhor os detalhes de baixo nível. Alex Darby tem uma série de artigos, onde ele orienta você com um depurador. Aqui está a parte 3 sobre a pilha. http://www.altdevblogaday.com/2011/12/14/cc-low-level-curriculum-part-3-the-stack/


O link parece estar inoperante, mas a verificação do Internet Archive Wayback Machine indica que ele fala apenas sobre a pilha e, portanto, não faz nada para responder à pergunta específica aqui de pilha versus pilha . -1
underscore_d
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.