Quando devo usar a nova palavra-chave em C ++?


273

Eu uso C ++ há um tempo e estou pensando sobre a nova palavra-chave. Simplesmente, devo usá-lo ou não?

1) Com a nova palavra-chave ...

MyClass* myClass = new MyClass();
myClass->MyField = "Hello world!";

2) Sem a nova palavra-chave ...

MyClass myClass;
myClass.MyField = "Hello world!";

Do ponto de vista da implementação, eles não parecem tão diferentes (mas tenho certeza de que são) ... No entanto, minha linguagem principal é C # e, é claro, o primeiro método é o que estou acostumado.

A dificuldade parece ser que o método 1 é mais difícil de usar com as classes C ++ padrão.

Qual método devo usar?

Atualização 1:

Recentemente, usei a nova palavra-chave para memória heap (ou armazenamento gratuito ) para uma matriz grande que estava fora do escopo (isto é, sendo retornada de uma função). Onde antes eu estava usando a pilha, que causava a corrupção de metade dos elementos fora do escopo, a mudança para o uso de heap garantia que os elementos estivessem intactos. Yay!

Atualização 2:

Um amigo meu recentemente me disse que há uma regra simples para usar a newpalavra - chave; toda vez que você digitar new, digite delete.

Foobar *foobar = new Foobar();
delete foobar; // TODO: Move this to the right place.

Isso ajuda a evitar vazamentos de memória, pois você sempre deve colocar a exclusão em algum lugar (por exemplo, quando você a recorta e cola em um destruidor ou outro).


6
A resposta curta é: use a versão curta quando puder se safar. :)
jalf

11
Uma técnica melhor do que sempre escrever uma exclusão correspondente - use contêineres STL e ponteiros inteligentes como std::vectore std::shared_ptr. Estes envoltório as chamadas para newe deletepara você, então você é ainda menos provável de vazamento de memória. Pergunte a si mesmo, por exemplo: você sempre se lembra de colocar uma correspondência em deletequalquer lugar em que uma exceção pudesse ser lançada? Colocar deletes à mão é mais difícil do que você imagina.
AshleysBrain

@nbolton Re: ATUALIZAÇÃO 1 - Uma das coisas bonitas sobre C ++ é que ele permite que você armazene tipos definidos pelo usuário na pilha, enquanto os langs coletados de lixo como o C # forçam você a armazenar os dados na pilha . Armazenar dados no heap consome mais recursos do que armazenar na pilha ; portanto, você deve preferir a pilha ao heap , exceto quando o seu UDT exigir uma grande quantidade de memória para armazenar seus dados. (Isso também significa que os objetos são passados ​​por valor por padrão). Uma solução melhor para o seu problema seria passar a matriz para a função por referência .
Charles Addis

Respostas:


304

Método 1 (usando new)

  • Aloca memória para o objeto no armazenamento gratuito (geralmente é a mesma coisa que a pilha )
  • Requer que você explique deleteseu objeto posteriormente. (Se você não excluí-lo, poderá criar um vazamento de memória)
  • A memória permanece alocada até você delete. (ou seja, você pode returncriar um objeto que você criou usando new)
  • O exemplo na pergunta vazará memória , a menos que o ponteiro seja deleted; e sempre deve ser excluído , independentemente de qual caminho de controle é usado ou se exceções são lançadas.

Método 2 (não usando new)

  • Aloca memória para o objeto na pilha (para onde vão todas as variáveis ​​locais) Geralmente, há menos memória disponível para a pilha; se você alocar muitos objetos, corre o risco de estourar a pilha.
  • Você não precisará deletemais tarde.
  • A memória não é mais alocada quando sai do escopo. (ou seja, você não deve returnapontar para um objeto na pilha)

Tanto quanto qual usar; você escolhe o método que funciona melhor para você, dadas as restrições acima.

Alguns casos fáceis:

  • Se você não quer se preocupar em ligar delete(e o potencial de causar vazamento de memória ), não deve usá-lo new.
  • Se você deseja retornar um ponteiro para o seu objeto a partir de uma função, use new

4
Um nitpick - acredito que o novo operador aloca memória de "armazenamento livre", enquanto o malloc aloca de "heap". Não é garantido que eles sejam a mesma coisa, embora na prática geralmente sejam. Veja gotw.ca/gotw/009.htm .
21711 Fred Larson

4
Eu acho que sua resposta poderia ser mais clara sobre qual usar. (99% do tempo, a escolha é simples Use o método 2, em um objeto wrapper que chama de novo / delete no construtor / destruidor.)
jalf

4
@ jalf: O método 2 é o que não usa o novo: - / Em qualquer caso, há muitas vezes que o código será muito mais simples (por exemplo, tratamento de casos de erro) usando o método 2 (aquele sem o novo)
Daniel LeCheminant 18/03/09

Outro detalhe ... Você deve deixar mais óbvio que o primeiro exemplo de Nick perde memória, enquanto o segundo não, mesmo diante de exceções.
Arafangion

4
@ Fred, Arafangion: Obrigado pela sua compreensão; Incorporamos seus comentários na resposta.
Daniel LeCheminant 24/03/09

118

Há uma diferença importante entre os dois.

Tudo que não é alocado com newse comporta de maneira semelhante aos tipos de valor em C # (e as pessoas costumam dizer que esses objetos são alocados na pilha, que é provavelmente o caso mais comum / óbvio, mas nem sempre é verdade. Mais precisamente, os objetos alocados sem o uso newtêm armazenamento automático duration Tudo o que é alocado com newé alocado no heap e um ponteiro para ele é retornado, exatamente como os tipos de referência em C #.

Qualquer coisa alocada na pilha precisa ter um tamanho constante, determinado em tempo de compilação (o compilador deve definir o ponteiro da pilha corretamente ou, se o objeto for membro de outra classe, deverá ajustar o tamanho dessa outra classe) . É por isso que matrizes em C # são tipos de referência. Eles precisam ser, porque com tipos de referência, podemos decidir em tempo de execução quanta memória pedir. E o mesmo se aplica aqui. Somente matrizes com tamanho constante (um tamanho que pode ser determinado em tempo de compilação) podem ser alocadas com duração de armazenamento automático (na pilha). Matrizes de tamanho dinâmico precisam ser alocadas no heap, chamando new.

(E é aí que qualquer semelhança com C # para)

Agora, qualquer coisa alocada na pilha tem duração de armazenamento "automático" (você pode realmente declarar uma variável como auto, mas esse é o padrão se nenhum outro tipo de armazenamento for especificado, para que a palavra-chave não seja realmente usada na prática, mas é aqui que ela é usada). vem de)

A duração do armazenamento automático significa exatamente o que parece, a duração da variável é tratada automaticamente. Por outro lado, qualquer coisa alocada no heap deve ser excluída manualmente por você. Aqui está um exemplo:

void foo() {
  bar b;
  bar* b2 = new bar();
}

Essa função cria três valores que vale a pena considerar:

Na linha 1, declara uma variável bdo tipo barna pilha (duração automática).

Na linha 2, declara um barponteiro b2na pilha (duração automática) e chama new, alocando um barobjeto no heap. (duração dinâmica)

Quando a função retorna, acontece o seguinte: Primeiro, b2fica fora do escopo (a ordem de destruição é sempre o oposto da ordem de construção). Mas b2é apenas um ponteiro, então nada acontece, a memória que ocupa é simplesmente liberada. E importante, a memória para a qual aponta (a barinstância na pilha) NÃO é tocada. Somente o ponteiro é liberado, porque somente o ponteiro possui duração automática. Segundo, bsai do escopo; portanto, como possui duração automática, seu destruidor é chamado e a memória é liberada.

E a barinstância na pilha? Provavelmente ainda está lá. Ninguém se incomodou em excluí-lo, então vazamos memória.

Neste exemplo, podemos ver que qualquer coisa com duração automática é garantida para ter seu destruidor chamado quando sai do escopo. Isso é útil. Mas qualquer coisa alocada no heap dura o tempo que for necessário e pode ser dimensionada dinamicamente, como no caso de matrizes. Isso também é útil. Podemos usar isso para gerenciar nossas alocações de memória. E se a classe Foo alocasse alguma memória na pilha em seu construtor e excluísse essa memória em seu destruidor. Poderíamos obter o melhor dos dois mundos, alocações de memória seguras que são garantidas para serem liberadas novamente, mas sem as limitações de forçar tudo a estar na pilha.

E é exatamente assim que funciona a maioria dos códigos C ++. Veja as bibliotecas padrão, std::vectorpor exemplo. Isso geralmente é alocado na pilha, mas pode ser dimensionado e redimensionado dinamicamente. E faz isso alocando internamente a memória na pilha, conforme necessário. O usuário da classe nunca vê isso; portanto, não há chance de vazar memória ou esquecer de limpar o que você alocou.

Esse princípio é chamado RAII (Aquisição de Recursos é Inicialização) e pode ser estendido a qualquer recurso que precise ser adquirido e liberado. (soquetes de rede, arquivos, conexões com o banco de dados, bloqueios de sincronização). Todos eles podem ser adquiridos no construtor e liberados no destruidor, para garantir que todos os recursos adquiridos serão liberados novamente.

Como regra geral, nunca use new / delete diretamente do seu código de alto nível. Sempre envolva-o em uma classe que possa gerenciar a memória para você e garantir que ela seja liberada novamente. (Sim, pode haver exceções a esta regra. Em particular, ponteiros inteligentes exigem que você chame newdiretamente e passe o ponteiro para seu construtor, que assume o controle e garante que deleteseja chamado corretamente. Mas essa ainda é uma regra muito importante )


2
"Tudo o que não é alocado com o novo é colocado na pilha" Não nos sistemas em que trabalhei ... geralmente dados inicializados (e não iniciados). Globais (estáticos) são colocados em seus próprios segmentos. Por exemplo, .data, .bss, etc ... segmentos de vinculador. Pedante, eu sei ...
Dan

Claro que você está certo. Eu realmente não estava pensando em dados estáticos. Meu mal, é claro. :)
jalf

2
Por que algo alocado na pilha precisa ter um tamanho constante?
user541686

Nem sempre , existem algumas maneiras de contornar isso, mas no caso geral, porque está em uma pilha. Se estiver no topo da pilha, pode ser possível redimensioná-la, mas quando outra coisa for colocada em cima dela, ela será "murada", cercada por objetos de ambos os lados, para que não possa ser redimensionada . Sim, dizer que sempre tem que ter um tamanho fixo é um pouco de simplificação, mas transmite a ideia básica (e eu não recomendaria mexer nas funções C que permitem que você seja criativo demais com alocações de pilha)
jalf

14

Qual método devo usar?

Isso quase nunca é determinado pelas suas preferências de digitação, mas pelo contexto. Se você precisar manter o objeto em algumas pilhas ou se for muito pesado para a pilha, aloque-o no armazenamento gratuito. Além disso, como você está alocando um objeto, você também é responsável por liberar a memória. Procure o deleteoperador.

Para aliviar o fardo de usar o gerenciamento de loja gratuita, as pessoas inventaram coisas como auto_ptre unique_ptr. Eu recomendo fortemente que você dê uma olhada neles. Eles podem até ajudar os seus problemas de digitação ;-)


10

Se você está escrevendo em C ++, provavelmente está escrevendo para obter desempenho. Usar o novo e o armazenamento gratuito é muito mais lento que o uso da pilha (especialmente ao usar threads); portanto, use-o somente quando necessário.

Como já foi dito, você precisa de algo novo quando seu objeto precisar viver fora da função ou do escopo do objeto, o objeto for realmente grande ou quando você não souber o tamanho de uma matriz em tempo de compilação.

Além disso, tente evitar o uso de excluir. Em vez disso, envolva o seu novo em um ponteiro inteligente. Deixe a chamada do ponteiro inteligente excluir para você.

Existem alguns casos em que um ponteiro inteligente não é inteligente. Nunca armazene std :: auto_ptr <> dentro de um contêiner STL. Ele excluirá o ponteiro muito cedo devido às operações de cópia dentro do contêiner. Outro caso é quando você tem um contêiner STL realmente grande de ponteiros para objetos. O boost :: shared_ptr <> terá uma tonelada de velocidade suspensa, pois aumenta a referência de contagem para cima e para baixo. A melhor maneira de ir nesse caso é colocar o contêiner STL em outro objeto e atribuir a esse objeto um destruidor que chamará delete em cada ponteiro no contêiner.


10

A resposta curta é: se você é iniciante em C ++, nunca deve usar newou a deletesi mesmo.

Em vez disso, você deve usar ponteiros inteligentes como std::unique_ptre std::make_unique(ou com menos frequência std::shared_ptre std::make_shared). Dessa forma, você não precisa se preocupar tanto com vazamentos de memória. E mesmo que você seja mais avançado, a melhor prática seria encapsular a maneira personalizada que você está usando newe deleteem uma classe pequena (como um ponteiro inteligente personalizado) que é dedicado apenas a problemas do ciclo de vida do objeto.

É claro que, nos bastidores, esses ponteiros inteligentes ainda estão realizando alocação e desalocação dinâmica; portanto, o código que os utiliza ainda teria o tempo de execução associado. Outras respostas aqui abordaram esses problemas e como tomar decisões de design sobre quando usar ponteiros inteligentes em vez de apenas criar objetos na pilha ou incorporá-los como membros diretos de um objeto, o suficiente para não repeti-los. Mas meu resumo executivo seria: não use indicadores inteligentes ou alocação dinâmica até que algo o force.


interessante ver como uma resposta pode mudar à medida que o tempo passa;)
Lobo


2

A resposta simples é sim - new () cria um objeto na pilha (com o efeito colateral lamentável de que você precisa gerenciar sua vida útil (chamando explicitamente a exclusão), enquanto o segundo formulário cria um objeto na pilha na atual escopo e esse objeto será destruído quando sair do escopo.


1

Se sua variável for usada apenas no contexto de uma única função, é melhor usar uma variável de pilha, ou seja, Opção 2. Como já foi dito, você não precisa gerenciar o tempo de vida das variáveis ​​de pilha - elas são construídas e destruído automaticamente. Além disso, a alocação / desalocação de uma variável no heap é lenta em comparação. Se sua função for chamada com bastante frequência, você verá uma tremenda melhoria de desempenho se usar variáveis ​​de pilha versus variáveis ​​de heap.

Dito isto, existem alguns casos óbvios em que as variáveis ​​da pilha são insuficientes.

Se a variável da pilha tiver um grande espaço de memória, você corre o risco de sobrecarregar a pilha. Por padrão, o tamanho da pilha de cada thread é de 1 MB no Windows. É improvável que você crie uma variável de pilha com 1 MB de tamanho, mas lembre-se de que a utilização da pilha é cumulativa. Se sua função chama uma função que chama outra função que chama outra função que ..., as variáveis ​​da pilha em todas essas funções ocupam espaço na mesma pilha. As funções recursivas podem enfrentar esse problema rapidamente, dependendo da profundidade da recursão. Se isso for um problema, você poderá aumentar o tamanho da pilha (não recomendado) ou alocar a variável no heap usando o novo operador (recomendado).

A outra condição mais provável é que sua variável precise "viver" além do escopo de sua função. Nesse caso, você alocaria a variável no heap para que ela possa ser alcançada fora do escopo de qualquer função.


1

Você está passando myClass de uma função ou espera que exista fora dessa função? Como alguns outros disseram, é tudo sobre escopo quando você não está alocando no heap. Quando você sai da função, ela desaparece (eventualmente). Um dos erros clássicos cometidos pelos iniciantes é a tentativa de criar um objeto local de alguma classe em uma função e devolvê-lo sem alocá-lo no heap. Lembro-me de depurar esse tipo de coisa nos meus dias anteriores em c ++.


0

O segundo método cria a instância na pilha, junto com itens declarados inte a lista de parâmetros que são passados ​​para a função.

O primeiro método abre espaço para um ponteiro na pilha, que você definiu para o local na memória em que um novo MyClassfoi alocado no heap - ou no armazenamento gratuito.

O primeiro método também exige que você deletecrie o que você cria new, enquanto no segundo método, a classe é automaticamente destruída e liberada quando fica fora do escopo (a próxima chave de fechamento, geralmente).


-1

A resposta curta é sim, a palavra-chave "nova" é incrivelmente importante, pois quando você a utiliza, os dados do objeto são armazenados na pilha, em oposição à pilha, o que é mais importante!

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.