Por que os programadores de C ++ deveriam minimizar o uso de 'novo'?


873

Eu me deparei com a pergunta Stack Overflow Vazamento de memória com std :: string ao usar std :: list <std :: string> , e um dos comentários diz o seguinte:

Pare de usar newtanto. Não vejo nenhum motivo para você ter usado novo em qualquer lugar. Você pode criar objetos por valor em C ++ e é uma das grandes vantagens de usar a linguagem.
Você não precisa alocar tudo no heap.
Pare de pensar como um programador Java .

Não tenho muita certeza do que ele quer dizer com isso.

Por que os objetos devem ser criados por valor em C ++ o mais rápido possível e que diferença faz internamente?
Interpretei mal a resposta?

Respostas:


1037

Existem duas técnicas de alocação de memória amplamente usadas: alocação automática e alocação dinâmica. Geralmente, há uma região correspondente da memória para cada um: a pilha e a pilha.

Pilha

A pilha sempre aloca memória de maneira seqüencial. Isso pode ser feito porque exige que você libere a memória na ordem inversa (primeiro a entrar, último a sair: FILO). Esta é a técnica de alocação de memória para variáveis ​​locais em muitas linguagens de programação. É muito, muito rápido, porque requer contabilidade mínima e o próximo endereço a ser alocado está implícito.

No C ++, isso é chamado de armazenamento automático porque o armazenamento é reivindicado automaticamente no final do escopo. Assim que a execução do bloco de código atual (usando delimitado {}) for concluída, a memória de todas as variáveis ​​desse bloco será coletada automaticamente. Este também é o momento em que os destruidores são chamados para limpar recursos.

Montão

A pilha permite um modo de alocação de memória mais flexível. A contabilidade é mais complexa e a alocação é mais lenta. Como não há ponto de liberação implícito, você deve liberar a memória manualmente, usando deleteou delete[]( freeem C). No entanto, a ausência de um ponto de liberação implícito é a chave para a flexibilidade do heap.

Razões para usar a alocação dinâmica

Mesmo que o uso do heap seja mais lento e potencialmente leve a vazamentos ou fragmentação da memória, há casos de uso perfeitamente bons para alocação dinâmica, pois é menos limitado.

Dois motivos principais para usar a alocação dinâmica:

  • Você não sabe quanta memória precisa em tempo de compilação. Por exemplo, ao ler um arquivo de texto em uma string, você geralmente não sabe qual o tamanho do arquivo, portanto, não pode decidir quanta memória alocar até executar o programa.

  • Você deseja alocar memória que persistirá após sair do bloco atual. Por exemplo, você pode escrever uma função string readfile(string path)que retorne o conteúdo de um arquivo. Nesse caso, mesmo que a pilha possa conter todo o conteúdo do arquivo, você não poderá retornar de uma função e manter o bloco de memória alocado.

Por que a alocação dinâmica geralmente é desnecessária

No C ++, há uma construção interessante chamada destruidor . Esse mecanismo permite gerenciar recursos alinhando a vida útil do recurso com a vida útil de uma variável. Essa técnica é chamada RAII e é o ponto distintivo do C ++. Ele "agrupa" recursos em objetos. std::stringé um exemplo perfeito. Este trecho:

int main ( int argc, char* argv[] )
{
    std::string program(argv[0]);
}

na verdade aloca uma quantidade variável de memória. O std::stringobjeto aloca memória usando a pilha e a libera em seu destruidor. Nesse caso, você não precisou gerenciar nenhum recurso manualmente e ainda obteve os benefícios da alocação dinâmica de memória.

Em particular, isso implica que neste trecho:

int main ( int argc, char* argv[] )
{
    std::string * program = new std::string(argv[0]);  // Bad!
    delete program;
}

há alocação de memória dinâmica desnecessária. O programa requer mais digitação (!) E apresenta o risco de esquecer de desalocar a memória. Faz isso sem nenhum benefício aparente.

Por que você deve usar o armazenamento automático o mais rápido possível

Basicamente, o último parágrafo resume. Usar o armazenamento automático o mais rápido possível cria seus programas:

  • mais rápido para digitar;
  • mais rápido quando executado;
  • menos propenso a vazamentos de memória / recursos.

Pontos bônus

Na pergunta referenciada, existem preocupações adicionais. Em particular, a seguinte classe:

class Line {
public:
    Line();
    ~Line();
    std::string* mString;
};

Line::Line() {
    mString = new std::string("foo_bar");
}

Line::~Line() {
    delete mString;
}

Na verdade, é muito mais arriscado do que o seguinte:

class Line {
public:
    Line();
    std::string mString;
};

Line::Line() {
    mString = "foo_bar";
    // note: there is a cleaner way to write this.
}

O motivo é que std::stringdefine corretamente um construtor de cópias. Considere o seguinte programa:

int main ()
{
    Line l1;
    Line l2 = l1;
}

Usando a versão original, este programa provavelmente trava, pois ele usa deletea mesma string duas vezes. Usando a versão modificada, cada Lineinstância terá sua própria instância de cadeia , cada uma com sua própria memória e ambas serão liberadas no final do programa.

Outras notas

O uso extensivo de RAII é considerado uma prática recomendada em C ++ por todos os motivos acima. No entanto, há um benefício adicional que não é imediatamente óbvio. Basicamente, é melhor que a soma de suas partes. Todo o mecanismo é composto . Escala.

Se você usar a Lineclasse como um bloco de construção:

 class Table
 {
      Line borders[4];
 };

Então

 int main ()
 {
     Table table;
 }

aloca quatro std::stringinstâncias, quatro Lineinstâncias, uma Tableinstância e todo o conteúdo da string e tudo é liberado automaticamente .


55
+1 por mencionar RAII no final, mas deve haver algo sobre exceções e desenrolamento da pilha.
Tobu

7
@ Tobu: sim, mas este post já é bastante longo, e eu queria mantê-lo bastante focado na pergunta da OP. Acabarei escrevendo uma postagem no blog ou algo assim e crie um link para ela a partir daqui.
André Caron

15
Seria um ótimo complemento para mencionar a desvantagem da alocação de pilha (pelo menos até C ++ 1x) - você geralmente precisará copiar as coisas desnecessariamente se não for cuidadoso. por exemplo, um Monstercospe a Treasurepara Worldquando morre. No seu Die()método, adiciona o tesouro ao mundo. Ele deve usar world->Add(new Treasure(/*...*/))em outro para preservar o tesouro depois que ele morre. As alternativas são shared_ptr(pode ser um exagero), auto_ptr(semântica ruim para transferência de propriedade), passa por valor (desperdício) e move+ unique_ptr(ainda não amplamente implementado).
kizzx2

7
O que você disse sobre variáveis ​​locais alocadas à pilha pode ser um pouco enganador. "A pilha" refere-se à pilha de chamadas, que armazena os quadros da pilha . São esses quadros de pilha que são armazenados da maneira LIFO. As variáveis ​​locais para um quadro específico são alocadas como se fossem membros de uma estrutura.
someguy

7
@someguy: De fato, a explicação não é perfeita. A implementação tem liberdade em sua política de alocação. No entanto, é necessário que as variáveis ​​sejam inicializadas e destruídas de uma maneira LIFO, de modo que a analogia se mantém. Acho que não é mais um trabalho que complica a resposta.
André Caron

171

Como a pilha é mais rápida e à prova de vazamentos

No C ++, são necessárias apenas uma única instrução para alocar espaço - na pilha - para cada objeto de escopo local em uma determinada função, e é impossível vazar parte dessa memória. Esse comentário pretendia (ou deveria ter pretendido) dizer algo como "use a pilha e não a pilha".


18
"é preciso apenas uma instrução para alocar espaço" - oh, bobagem. Certamente, são necessárias apenas uma instrução para adicionar ao ponteiro da pilha, mas se a classe tiver alguma estrutura interna interessante, haverá muito mais do que adicionar ao ponteiro da pilha em andamento. É igualmente válido dizer que em Java não são necessárias instruções para alocar espaço, porque o compilador gerenciará as referências em tempo de compilação.
Charlie

31
@Charlie está correto. Variáveis ​​automáticas são rápidas e infalíveis seriam mais precisas.
Oliver Charlesworth

28
@ Charlie: Os internos da classe precisam ser configurados de qualquer maneira. A comparação está sendo feita na alocação do espaço necessário.
26711 Oliver Oliverworth

51
tosse int x; return &x;
peterchen

16
rápido sim. Mas certamente não é infalível. Nada é infalível. Você pode obter uma StackOverflow :)
rxantos

107

A razão pela qual é complicada.

Primeiro, o C ++ não é coletado como lixo. Portanto, para cada novo, deve haver uma exclusão correspondente. Se você não colocar essa exclusão, há um vazamento de memória. Agora, para um caso simples como este:

std::string *someString = new std::string(...);
//Do stuff
delete someString;

Isto é simples. Mas o que acontece se "Fazer coisas" lançar uma exceção? Ops: vazamento de memória. O que acontece se problemas com "Fazer coisas"return cedo? Ops: vazamento de memória.

E isso é para o caso mais simples . Se você devolver essa string a alguém, agora eles deverão excluí-la. E se eles passarem como argumento, a pessoa que o recebe precisa excluí-lo? Quando eles devem excluí-lo?

Ou, você pode apenas fazer o seguinte:

std::string someString(...);
//Do stuff

Não delete . O objeto foi criado na "pilha" e será destruído quando ficar fora do escopo. Você pode até retornar o objeto, transferindo seu conteúdo para a função de chamada. Você pode passar o objeto para funções (normalmente como uma referência ou referência constante:void SomeFunc(std::string &iCanModifyThis, const std::string &iCantModifyThis) E assim por diante.

Tudo sem newe delete. Não há dúvida de quem possui a memória ou quem é responsável por excluí-la. Se você fizer:

std::string someString(...);
std::string otherString;
otherString = someString;

Entende-se que otherStringpossui uma cópia dos dados de someString. Não é um ponteiro; é um objeto separado. Eles podem ter o mesmo conteúdo, mas você pode alterar um sem afetar o outro:

someString += "More text.";
if(otherString == someString) { /*Will never get here */ }

Veja a ideia?


1
Na mesma nota ... Se um objeto é alocado dinamicamente main(), existe durante o programa, não pode ser facilmente criado na pilha devido à situação, e os ponteiros para ele são passados ​​para quaisquer funções que exijam acesso a ele , isso pode causar um vazamento no caso de uma falha no programa ou seria seguro? Eu assumiria o último, já que o SO que desaloca toda a memória do programa também deve desalocá-lo logicamente, mas não quero assumir nada quando se trata new.
Justin Time - Restabelece Monica

4
@ JustinTime Você não precisa se preocupar em liberar memória de objetos alocados dinamicamente, que devem permanecer durante toda a vida útil do programa. Quando um programa é executado, o sistema operacional cria um atlas de memória física, ou Memória Virtual, para ele. Todo endereço no espaço de memória virtual é mapeado para um endereço de memória física e, quando o programa sai, tudo o que é mapeado para sua memória virtual é liberado. Portanto, desde que o programa saia completamente, você não precisa se preocupar com a memória alocada que nunca será excluída.
Aiman ​​Al-Eryani

75

Objetos criados por newdevem eventualmente ser usados deletepara que não vazem. O destruidor não será chamado, a memória não será liberada, o tempo todo. Como o C ++ não tem coleta de lixo, é um problema.

Objetos criados por valor (ou seja, na pilha) morrem automaticamente quando ficam fora do escopo. A chamada de destruidor é inserida pelo compilador e a memória é liberada automaticamente ao retornar a função.

Ponteiros inteligentes como unique_ptr,shared_ptr resolvem o problema de referência pendente, mas exigem disciplina de codificação e têm outros problemas em potencial (copiabilidade, loops de referência etc.).

Além disso, em cenários altamente multithread, newhá um ponto de discórdia entre threads; pode haver um impacto no desempenho por uso excessivonew . A criação do objeto de pilha é por definição thread-local, pois cada thread tem sua própria pilha.

A desvantagem dos objetos de valor é que eles morrem quando a função host retorna - você não pode passar uma referência para os que retornam ao chamador, apenas copiando, retornando ou movendo-se por valor.


9
+1. Re "Objetos criados por newdevem eventualmente ser usados deletepara que não vazem". - pior ainda, new[]deve ser correspondido delete[]e você terá um comportamento indefinido se você tem delete new[]memória ou delete[] newmemória - muito poucos compiladores alertam sobre isso (algumas ferramentas como o Cppcheck fazem quando podem).
Tony Delroy

3
@TonyDelroy Há situações em que o compilador não pode avisar isso. Se uma função retornar um ponteiro, ela poderá ser criada se nova (um único elemento) ou nova [].
Fbafelipe 27/06

32
  • O C ++ não emprega nenhum gerenciador de memória sozinho. Outras linguagens como C #, Java possui coletor de lixo para lidar com a memória
  • As implementações de C ++ geralmente usam rotinas de sistema operacional para alocar a memória e muito novo / exclusão pode fragmentar a memória disponível
  • Em qualquer aplicativo, se a memória estiver sendo usada com frequência, é aconselhável pré-alocá-la e liberá-la quando não for necessária.
  • O gerenciamento inadequado da memória pode levar a vazamentos de memória e é realmente difícil de rastrear. Portanto, usar objetos de pilha dentro do escopo da função é uma técnica comprovada
  • A desvantagem de usar objetos de pilha é que ele cria várias cópias de objetos ao retornar, passar para funções etc. No entanto, os compiladores inteligentes conhecem bem essas situações e foram otimizados para desempenho.
  • É realmente tedioso em C ++ se a memória está sendo alocada e liberada em dois lugares diferentes. A responsabilidade pela liberação é sempre uma pergunta e, em geral, contamos com alguns ponteiros comumente acessíveis, objetos de pilha (máximo possível) e técnicas como auto_ptr (objetos RAII)
  • O melhor é que você tem controle sobre a memória e o pior é que não terá controle sobre a memória se empregarmos um gerenciamento inadequado de memória para o aplicativo. As falhas causadas por corrupção de memória são as mais desagradáveis ​​e difíceis de rastrear.

5
Na verdade, qualquer idioma que aloque memória possui um gerenciador de memória, incluindo c. A maioria é simplesmente muito simples, ou seja, int * x = malloc (4); int * y = malloc (4); ... a primeira chamada alocará memória, também conhecida como pedir memória do sistema operacional (geralmente em partes 1k / 4k), para que a segunda chamada não aloque memória de fato, mas fornece um pedaço do último pedaço que ele alocou. Na IMO, os coletores de lixo não são gerenciadores de memória, porque ele lida apenas com a desalocação automática da memória. Para ser chamado de gerenciador de memória, ele não deve apenas lidar com a desalocação, mas também com a alocação de memória.
Rahly

1
Variáveis ​​locais usam a pilha para que o compilador não emita chamada malloc()ou para seus amigos para alocar a memória necessária. No entanto, a pilha não pode liberar nenhum item dentro da pilha, a única maneira pela qual a memória é liberada é desenrolando da parte superior da pilha.
Mikko Rantalainen 12/10

C ++ não "usa rotinas de sistema operacional"; isso não faz parte da linguagem, é apenas uma implementação comum. C ++ pode até estar em execução sem qualquer sistema operacional.
Einpoklum 28/07/19

22

Vejo que algumas razões importantes para fazer o mínimo possível de novidades são perdidas:

O operador newpossui um tempo de execução não determinístico

A chamada newpode ou não fazer com que o sistema operacional aloque uma nova página física para o seu processo. Isso pode ser bastante lento se você fizer isso com frequência. Ou talvez já tenha um local de memória adequado pronto, não sabemos. Se o seu programa precisa ter um tempo de execução consistente e previsível (como em um sistema em tempo real ou simulação de jogo / física), você precisa evitar newem seus ciclos críticos de tempo.

Operador newé uma sincronização implícita de encadeamentos

Sim, você me ouviu, seu sistema operacional precisa garantir que suas tabelas de páginas sejam consistentes e, como tal, a chamada newfará com que seu encadeamento adquira um bloqueio mutex implícito. Se você está constantemente chamando a newpartir de muitos threads, na verdade, está serializando seus threads (eu fiz isso com 32 CPUs, cada uma pressionando newpara obter algumas centenas de bytes cada, ai! Isso foi uma pita real para depurar)

O restante, como lento, fragmentação, propenso a erros, etc, já foram mencionados por outras respostas.


2
Ambos podem ser evitados usando a colocação new / delete e alocando a memória antes da mão. Ou você pode alocar / liberar a memória e chamar o construtor / destruidor. É assim que o std :: vector geralmente funciona.
rxantos 12/02

1
@rxantos Por favor, leia OP, esta questão é sobre como evitar alocações de memória desnecessárias. Além disso, não há exclusão de canal.
Emily L.

@Emily Isto é o que o OP significava, eu acho:void * someAddress = ...; delete (T*)someAddress
xryl669

1
O uso da pilha também não é determinístico no tempo de execução. A menos que você tenha ligado mlock()ou algo semelhante. Isso ocorre porque o sistema pode estar com pouca memória e não há páginas de memória física disponíveis para a pilha; portanto, o sistema operacional pode precisar trocar ou gravar alguns caches (limpar a memória suja) no disco antes que a execução possa prosseguir.
Mikko Rantalainen 12/10

1
@mikkorantalainen isso é tecnicamente verdade, mas em uma situação de pouca memória, todas as apostas são desativadas de qualquer maneira, com o desempenho incorreto conforme você pressiona o disco, para que não haja nada que você possa fazer. De qualquer forma, não invalida o conselho para evitar novas chamadas quando for razoável fazê-lo.
Emily L.

21

Pré-C ++ 17:

Porque é propenso a vazamentos sutis, mesmo que você envolva o resultado em um ponteiro inteligente .

Considere um usuário "cuidadoso" que se lembre de agrupar objetos em ponteiros inteligentes:

foo(shared_ptr<T1>(new T1()), shared_ptr<T2>(new T2()));

Esse código é perigoso, porque não há garantia de que um shared_ptrseja construído antes de um T1ou outro T2. Portanto, se um new T1()ou new T2()falhar após o outro for bem-sucedido, o primeiro objeto será vazado porque não shared_ptrexiste para destruí-lo e desalocá-lo.

Solução: use make_shared.

Pós-C ++ 17:

Isso não é mais um problema: o C ++ 17 impõe uma restrição à ordem dessas operações, neste caso, garantindo que cada chamada new()seja seguida imediatamente pela construção do ponteiro inteligente correspondente, sem nenhuma outra operação no meio. Isso implica que, quando o segundo new()for chamado, é garantido que o primeiro objeto já esteja envolto em seu ponteiro inteligente, evitando assim vazamentos no caso de uma exceção ser lançada.

Uma explicação mais detalhada da nova ordem de avaliação introduzida pelo C ++ 17 foi fornecida por Barry em outra resposta .

Agradecemos a @Remy Lebeau por apontar que este ainda é um problema no C ++ 17 (embora menos): o shared_ptrconstrutor pode falhar ao alocar seu bloco de controle e o lançamento, caso em que o ponteiro passado para ele não é excluído.

Solução: use make_shared.


5
Outra solução: nunca aloque dinamicamente mais de um objeto por linha.
Antimony

3
@Antimony: Sim, é muito mais tentador alocar mais de um objeto quando você já tiver alocado um, em comparação com quando você não alocou nenhum.
user541686

1
Penso que uma resposta melhor é que o smart_ptr vazará se uma exceção for chamada e nada a capturar.
Natalie Adams

2
Mesmo no caso pós-C ++ 17, um vazamento ainda pode ocorrer se newfor bem-sucedido e a shared_ptrconstrução subsequente falhar. std::make_shared()resolveria isso também
Remy Lebeau

1
@Mehrdad, o shared_ptrconstrutor em questão aloca memória para um bloco de controle que armazena o ponteiro e o deleter compartilhado; portanto, sim, teoricamente, ele pode gerar um erro de memória. Somente os construtores de copiar, mover e aliasing não são lançados. make_sharedaloca o objeto compartilhado dentro do próprio bloco de controle, para que haja apenas 1 alocação em vez de 2.
Remy Lebeau

17

Em grande parte, é alguém que eleva suas próprias fraquezas a uma regra geral. Não há nada errado per se com a criação de objetos usando o newoperador. O que há para argumentar é que você precisa fazer isso com alguma disciplina: se você criar um objeto, precisará garantir que ele seja destruído.

A maneira mais fácil de fazer isso é criar o objeto no armazenamento automático, para que o C ++ saiba destruí-lo quando sair do escopo:

 {
    File foo = File("foo.dat");

    // do things

 }

Agora, observe que quando você cai desse bloco após o fim da órbita, fooestá fora do escopo. O C ++ chamará seu dtor automaticamente para você. Ao contrário do Java, você não precisa esperar o GC encontrá-lo.

Você escreveu

 {
     File * foo = new File("foo.dat");

você gostaria de combiná-lo explicitamente com

     delete foo;
  }

ou melhor ainda, aloque o seu File *como um "ponteiro inteligente". Se você não tomar cuidado com isso, pode levar a vazamentos.

A resposta em si assume o equívoco de que, se você não usar new, não o alocará na pilha; de fato, em C ++ você não sabe disso. No máximo, você sabe que um pouco de memória, digamos um ponteiro, certamente está alocado na pilha. No entanto, considere se a implementação do File é algo como

  class File {
    private:
      FileImpl * fd;
    public:
      File(String fn){ fd = new FileImpl(fn);}

em seguida, FileImplvai ainda ser alocada na pilha.

E sim, é melhor você ter certeza de ter

     ~File(){ delete fd ; }

na classe também; sem ele, você vazará memória do heap, mesmo que aparentemente não tenha sido alocado no heap.


4
Você deve dar uma olhada no código na pergunta referenciada. Definitivamente, há muitas coisas dando errado nesse código.
André Caron

7
Concordo que não há nada de errado em usar new per se , mas se você olhar o código original ao qual o comentário se refere, newestá sendo abusado. O código é escrito como se fosse Java ou C #, onde newé usado para praticamente todas as variáveis, quando as coisas fazem muito mais sentido estar na pilha.
luke

5
Ponto justo. Mas regras gerais são normalmente aplicadas para evitar armadilhas comuns. Se isso foi uma fraqueza individual ou não, o gerenciamento de memória é complexo o suficiente para garantir uma regra geral como essa! :)
Robben_Ford_Fan_boy

9
@ Charlie: o comentário não diz que você nunca deve usar new. Ele diz que se você tiver a opção entre alocação dinâmica e armazenamento automático, use o armazenamento automático.
André Caron

8
@ Charlie: não há nada errado com o uso new, mas se você usar delete, você está fazendo errado!
Matthieu M.

16

new()não deve ser usado o menos possível. Deve ser usado com o maior cuidado possível. E deve ser usado quantas vezes for necessário, ditado pelo pragmatismo.

A alocação de objetos na pilha, contando com sua destruição implícita, é um modelo simples. Se o escopo necessário de um objeto se encaixa nesse modelo, não há necessidade de usar new(), com a associação delete()e verificação de ponteiros NULL. No caso em que você tem uma alocação muito curta de objetos na pilha, reduz os problemas de fragmentação de heap.

No entanto, se a vida útil do seu objeto precisar se estender além do escopo atual, new()é a resposta certa. Apenas preste atenção em quando e como você chama delete()e as possibilidades de ponteiros NULL, usando objetos excluídos e todas as outras dicas que acompanham o uso de ponteiros.


9
"se a vida útil do seu objeto precisar se estender além do escopo atual, então new () é a resposta certa" ... por que não retornar preferencialmente por valor ou aceitar uma variável com escopo de chamador por não- constreferência ou ponteiro ...?
Tony Delroy

2
@ Tony: Sim, sim! Fico feliz em ouvir alguém advogando referências. Eles foram criados para evitar esse problema.
Nathan Osman

@TonyD ... ou combine-os: retorne um ponteiro inteligente por valor. Dessa forma, o chamador e, em muitos casos (ou seja, onde make_shared/_uniqueé utilizável), o receptor nunca precisa newou delete. Essa resposta ignora os pontos reais: (A) O C ++ fornece coisas como RVO, semântica de movimentação e parâmetros de saída - o que geralmente significa que lidar com a criação de objetos e a extensão da vida útil retornando memória alocada dinamicamente se torna desnecessário e descuidado. (B) Mesmo em situações em que a alocação dinâmica é necessária, o stdlib fornece wrappers RAII que aliviam o usuário dos detalhes internos feios.
Sublinhado_

14

Quando você usa novo, os objetos são alocados ao heap. Geralmente é usado quando você antecipa a expansão. Quando você declara um objeto como,

Class var;

é colocado na pilha.

Você sempre precisará chamar de destruir o objeto que colocou na pilha com novo. Isso abre o potencial para vazamentos de memória. Objetos colocados na pilha não são propensos a vazamento de memória!


2
+1 "[heap] geralmente usado quando você antecipa a expansão" - como anexar a um std::stringou std::map, sim, um insight profundo. Minha reação inicial foi "mas também muito comumente dissociar a vida útil de um objeto do escopo do código de criação", mas realmente retornar por valor ou aceitar valores com escopo de chamador por não constreferência ou ponteiro é melhor para isso, exceto quando há "expansão" envolvida também. Há alguns outros usos de som como métodos de fábrica embora ....
Tony Delroy

12

Um motivo notável para evitar o uso excessivo da pilha é o desempenho - envolvendo especificamente o desempenho do mecanismo de gerenciamento de memória padrão usado pelo C ++. Enquanto alocação pode ser bastante rápida no caso trivial, fazendo um monte de newe deleteem objetos de tamanho não uniforme sem estritas leads fim não só de fragmentação de memória, mas também complica o algoritmo de alocação e pode absolutamente destruir desempenho em determinados casos.

Esse é o problema que os pools de memória foram criados para resolver, permitindo mitigar as desvantagens inerentes às implementações de heap tradicionais, enquanto ainda permitem que você use o heap conforme necessário.

Melhor ainda, porém, evitar o problema completamente. Se você pode colocá-lo na pilha, faça-o.


Você sempre pode alocar uma quantidade razoavelmente grande de memória e usar o posicionamento new / delete se a velocidade for um problema.
rxantos 12/02

Os pools de memória são para evitar fragmentação, acelerar a desalocação (uma desalocação para milhares de objetos) e tornar a desalocação mais segura.
Lothar

10

Eu acho que o cartaz quis dizer You do not have to allocate everything on theheapmais do que o stack.

Basicamente, os objetos são alocados na pilha (se o tamanho do objeto permitir, é claro) por causa do custo barato da alocação de pilha, em vez da alocação baseada em heap, que envolve bastante trabalho do alocador e adiciona verbosidade, pois é necessário gerenciar dados alocados no heap.


10

Costumo discordar da idéia de usar novos "demais". Embora o uso do novo original pelas classes do sistema seja um pouco ridículo. ( int *i; i = new int[9999];? realmente? int i[9999];é muito mais claro.) Acho que foi isso que estava deixando a cabra do comentarista.

Quando você trabalha com objetos do sistema, é muito raro precisar de mais de uma referência ao mesmo objeto. Contanto que o valor seja o mesmo, isso é tudo o que importa. E os objetos do sistema normalmente não ocupam muito espaço na memória. (um byte por caractere, em uma sequência). E, se o fizerem, as bibliotecas devem ser projetadas para levar em consideração esse gerenciamento de memória (se forem bem escritas). Nesses casos, (com exceção de uma ou duas das notícias em seu código), new é praticamente inútil e serve apenas para introduzir confusões e potencial para erros.

Entretanto, quando você trabalha com suas próprias classes / objetos (por exemplo, a classe Line do pôster original), deve começar a pensar em questões como pegada de memória, persistência de dados etc. Nesse ponto, permitir várias referências ao mesmo valor é inestimável - ele permite construções como listas, dicionários e gráficos vinculados, nas quais várias variáveis ​​precisam não apenas ter o mesmo valor, mas fazer referência exatamente ao mesmo objeto na memória. No entanto, a classe Line não possui nenhum desses requisitos. Portanto, o código do pôster original não tem absolutamente nenhuma necessidade new.


geralmente o novo / delete seria usado quando você não souber de antemão o tamanho da matriz. É claro que std :: vector oculta new / delete para você. Você ainda os usa, mas através do std :: vector. Portanto, hoje em dia seria usado quando você não sabe o tamanho da matriz e deseja, por algum motivo, evitar a sobrecarga do std :: vector (que é pequeno, mas ainda existe).
rxantos 12/02/2015

When you're working with your own classes/objects... você muitas vezes não tem motivos para fazê-lo! Uma pequena proporção de Qs está nos detalhes do design de contêineres por codificadores especializados. Em contraste, uma proporção deprimente é sobre a confusão de novatos que não sabem que o stdlib existe - ou recebem ativamente tarefas terríveis em cursos de 'programação', onde um tutor exige que eles reinventem a roda sem sentido - antes mesmo de aprendeu o que é uma roda e por que ela funciona. Ao promover uma alocação mais abstrata, o C ++ pode nos salvar do infinito 'segfault com lista vinculada' do C; por favor, vamos deixar .
Sublinhado_

" o uso do novo pôster original com classes de sistema é um pouco ridículo. ( int *i; i = new int[9999];? realmente? int i[9999];é muito mais claro.)" Sim, é mais claro, mas para interpretar o advogado do diabo, o tipo não é necessariamente um argumento ruim. Com 9999 elementos, posso imaginar um sistema incorporado rígido que não tenha pilha suficiente para 9999 elementos: bytes de 9999x4 são ~ 40 kB, x8 ~ 80 kB. Portanto, esses sistemas podem precisar usar alocação dinâmica, supondo que eles a implementem usando memória alternativa. Ainda assim, isso poderia justificar apenas a alocação dinâmica, não new; um vectorseria o verdadeiro correção nesse caso
underscore_d

Concorde com @underscore_d - esse não é um exemplo tão bom. Eu não adicionaria 40.000 ou 80.000 bytes à minha pilha assim. Na verdade, eu provavelmente os alocaria na pilha (com é std::make_unique<int[]>()claro).
Einpoklum 28/07/19

3

Duas razões:

  1. É desnecessário neste caso. Você está tornando seu código desnecessariamente mais complicado.
  2. Aloca espaço no heap e significa que você precisa se lembrar deletemais tarde, ou isso causará um vazamento de memória.

2

newé o novo goto.

Lembre-se do motivo de gotoser tão criticado: embora seja uma ferramenta poderosa e de baixo nível para controle de fluxo, as pessoas costumavam usá-lo de maneiras desnecessariamente complicadas que dificultavam o código. Além disso, os padrões mais úteis e fáceis de ler foram codificados em instruções de programação estruturada (por exemplo, forou while); o efeito final é que o código para onde gotoé apropriado é bastante raro; se você é tentado a escrever goto, provavelmente está fazendo coisas ruins (a menos que saiba realmente o que está fazendo).

newé semelhante - geralmente é usado para tornar as coisas desnecessariamente complicadas e difíceis de ler, e os padrões de uso mais úteis que podem ser codificados foram codificados em várias classes. Além disso, se você precisar usar novos padrões de uso para os quais ainda não existem classes padrão, você pode escrever suas próprias classes que as codificam!

Eu diria até que isso newé pior do que gotodevido à necessidade de emparelhar newe deletedeclarações.

Por exemplo goto, se você acha que precisa usá- newlo, provavelmente está fazendo coisas ruins - especialmente se estiver fazendo isso fora da implementação de uma classe cujo objetivo na vida é encapsular as alocações dinâmicas que você precisa fazer.


E eu acrescentaria: "Você basicamente não precisa disso".
Einpoklum 28/07/19

1

O principal motivo é que os objetos na pilha são sempre difíceis de usar e gerenciar do que valores simples. Escrever código fácil de ler e manter é sempre a primeira prioridade de qualquer programador sério.

Outro cenário é a biblioteca que estamos usando fornece semântica de valores e torna desnecessária a alocação dinâmica. Std::stringé um bom exemplo

No entanto, para código orientado a objetos, é necessário usar um ponteiro - o que significa usar newpara criá-lo antecipadamente. Para simplificar a complexidade do gerenciamento de recursos, temos dezenas de ferramentas para torná-lo o mais simples possível, como indicadores inteligentes. O paradigma baseado em objeto ou paradigma genérico assume semântica de valor e requer menos ou não new, assim como os pôsteres declarados em outro lugar.

Os padrões de design tradicionais, especialmente aqueles mencionados no livro GoF , usam newmuito, pois são códigos OO típicos.


4
Esta é uma resposta abismal . For object oriented code, using a pointer [...] is a must: absurdo . Se você está desvalorizando 'OO', referindo-se apenas a um pequeno subconjunto, o polimorfismo - também é um absurdo: as referências também funcionam. [pointer] means use new to create it beforehand: especialmente bobagem : referências ou ponteiros podem ser usados ​​para objetos alocados automaticamente e usados ​​polimorficamente; me observe . [typical OO code] use new a lot: talvez em algum livro antigo, mas quem se importa? Qualquer vagamente moderno C ++ evita new/ ponteiros crus sempre que possível - e é de nenhuma maneira menos OO ao fazê-lo
underscore_d

1

Mais um ponto para todas as respostas corretas acima, depende de que tipo de programação você está fazendo. Kernel em desenvolvimento no Windows, por exemplo -> A pilha é severamente limitada e talvez você não consiga corrigir falhas de página como no modo de usuário.

Nesses ambientes, chamadas de API novas ou do tipo C são preferidas e até necessárias.

Obviamente, isso é apenas uma exceção à regra.


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.