Instanciação de objeto C ++

113

Sou um programador C tentando entender C ++. Muitos tutoriais demonstram a instanciação de objetos usando um snippet como:

Dog* sparky = new Dog();

o que implica que mais tarde você fará:

delete sparky;

o que faz sentido. Agora, no caso em que a alocação de memória dinâmica é desnecessária, há alguma razão para usar o acima em vez de

Dog sparky;

e deixar o destruidor ser chamado assim que o sparky sair do escopo?

Obrigado!

el_champo
fonte

Respostas:

166

Ao contrário, você deve sempre preferir alocações de pilha, na medida em que, como regra geral, você nunca deve ter new / delete em seu código de usuário.

Como você disse, quando a variável é declarada na pilha, seu destruidor é automaticamente chamado quando sai do escopo, que é sua principal ferramenta para rastrear o tempo de vida do recurso e evitar vazamentos.

Portanto, em geral, toda vez que você precisa alocar um recurso, seja memória (chamando new), identificadores de arquivo, soquetes ou qualquer outra coisa, envolva-o em uma classe onde o construtor adquire o recurso e o destruidor o libera. Em seguida, você pode criar um objeto desse tipo na pilha e tem a garantia de que seu recurso será liberado quando sair do escopo. Dessa forma, você não precisa rastrear seus pares novos / deletar em todos os lugares para evitar vazamentos de memória.

O nome mais comum para este idioma é RAII

Observe também as classes de ponteiros inteligentes que são usadas para envolver os ponteiros resultantes nos raros casos em que você precisa alocar algo com novo fora de um objeto RAII dedicado. Em vez disso, você passa o ponteiro para um ponteiro inteligente, que então rastreia seu tempo de vida, por exemplo, por contagem de referência, e chama o destruidor quando a última referência sai do escopo. A biblioteca padrão tem std::unique_ptrum gerenciamento simples baseado em escopo e std::shared_ptrque faz a contagem de referência para implementar a propriedade compartilhada.

Muitos tutoriais demonstram a instanciação de objetos usando um snippet como ...

Então o que você descobriu é que a maioria dos tutoriais é uma merda. ;) A maioria dos tutoriais ensina práticas ruins de C ++, incluindo chamar new / delete para criar variáveis ​​quando não for necessário, e dificultando o controle do tempo de vida de suas alocações.

Jalf
fonte
Os ponteiros brutos são úteis quando você deseja uma semântica semelhante a auto_ptr (transferência de propriedade), mas mantém a operação de troca sem lançamento e não deseja a sobrecarga da contagem de referência. Caso extremo talvez, mas útil.
Greg Rogers
2
Esta é uma resposta correta, mas o motivo pelo qual eu nunca teria o hábito de criar objetos na pilha é porque nunca é completamente óbvio o quão grande esse objeto será. Você está apenas pedindo uma exceção de estouro de pilha.
dviljoen
3
Greg: Certamente. Mas, como você disse, um caso extremo. Em geral, é melhor evitar ponteiros. Mas eles estão na língua por uma razão, não há como negar isso. :) dviljoen: Se o objeto for grande, você o envolve em um objeto RAII, que pode ser alocado na pilha e contém um ponteiro para os dados alocados no heap.
jalf
3
@dviljoen: Não, não estou. Os compiladores C ++ não incham objetos desnecessariamente. O pior que você verá é tipicamente que ele é arredondado para o múltiplo de quatro bytes mais próximo. Normalmente, uma classe que contém um ponteiro ocupará tanto espaço quanto o próprio ponteiro, portanto, não custa nada no uso da pilha.
jalf de
20

Embora ter coisas na pilha possa ser uma vantagem em termos de alocação e liberação automática, tem algumas desvantagens.

  1. Você pode não querer alocar objetos enormes na Pilha.

  2. Envio dinâmico! Considere este código:

#include <iostream>

class A {
public:
  virtual void f();
  virtual ~A() {}
};

class B : public A {
public:
  virtual void f();
};

void A::f() {cout << "A";}
void B::f() {cout << "B";}

int main(void) {
  A *a = new B();
  a->f();
  delete a;
  return 0;
}

Isso imprimirá "B". Agora vamos ver o que acontece ao usar o Stack:

int main(void) {
  A a = B();
  a.f();
  return 0;
}

Isso imprimirá "A", o que pode não ser intuitivo para aqueles que estão familiarizados com Java ou outras linguagens orientadas a objetos. O motivo é que você não tem mais um ponteiro para uma instância B. Em vez disso, uma instância de Bé criada e copiada para a avariável do tipo A.

Algumas coisas podem acontecer de forma não intuitiva, especialmente quando você é novo em C ++. Em C você tem suas dicas e é isso. Você sabe como usá-los e eles SEMPRE fazem o mesmo. Em C ++, esse não é o caso. Imagine o que acontece, quando você usa um neste exemplo como um argumento para um método - as coisas ficam mais complicadas e FAZ uma grande diferença se aé do tipo Aou A*ou mesmo A&(chamada por referência). Muitas combinações são possíveis e todas se comportam de maneira diferente.

Universo
fonte
2
-1: as pessoas não são capazes de entender a semântica de valores e o simples fato de que o polimorfismo precisa de uma referência / ponteiro (para evitar o fatiamento de objetos) não constitui nenhum tipo de problema com a linguagem. O poder do c ++ não deve ser considerado uma desvantagem apenas porque algumas pessoas não conseguem aprender suas regras básicas.
sublinhado_d
Obrigado pelo aviso. Eu tive um problema semelhante com o método tomando em objeto em vez de ponteiro ou referência, e que bagunça era. O objeto possui ponteiros e eles foram excluídos muito cedo devido a este enfrentamento.
BoBoDev
@underscore_d Eu concordo; o último parágrafo desta resposta deve ser removido. Não pense que o fato de eu ter editado essa resposta significa que concordo com ela; Eu só não queria que os erros fossem lidos.
TamaMcGlinn 01 de
@TamaMcGlinn concordou. Obrigado pela boa edição. Removi a parte da opinião.
UniversE 01 de
13

Bem, a razão para usar o ponteiro seria exatamente a mesma que a razão para usar ponteiros em C alocados com malloc: se você quiser que seu objeto viva mais do que sua variável!

É ainda altamente recomendado NÃO usar o novo operador se você puder evitá-lo. Especialmente se você usar exceções. Em geral, é muito mais seguro deixar o compilador liberar seus objetos.

PierreBdR
fonte
13

Já vi esse antipadrão de pessoas que não entendem muito bem o operador & address-of. Se eles precisarem chamar uma função com um ponteiro, eles sempre alocarão no heap para obter um ponteiro.

void FeedTheDog(Dog* hungryDog);

Dog* badDog = new Dog;
FeedTheDog(badDog);
delete badDog;

Dog goodDog;
FeedTheDog(&goodDog);
Mark Ransom
fonte
Alternativamente: void FeedTheDog (Dog &gryDog); Cão Cão; FeedTheDog (cachorro);
Scott Langham
1
@ScottLangham verdade, mas não era esse o ponto que eu estava tentando mostrar. Às vezes, você está chamando uma função que leva um ponteiro NULL opcional, por exemplo, ou talvez seja uma API existente que não pode ser alterada.
Mark Ransom
7

Trate a pilha como um bem importante e use-a com muito cuidado. A regra básica é usar pilha sempre que possível e usar heap sempre que não houver outra maneira. Ao alocar os objetos na pilha, você pode obter muitos benefícios, como:

(1). Você não precisa se preocupar com o desenrolamento da pilha em caso de exceções

(2). Você não precisa se preocupar com a fragmentação da memória causada pela alocação de mais espaço do que o necessário por seu gerenciador de heap.

Naveen
fonte
1
Deve haver alguma consideração quanto ao tamanho do objeto. O tamanho da pilha é limitado, portanto, objetos muito grandes devem ser alocados no heap.
Adam
1
@Adam Acho que Naveen ainda está correto aqui. Se você pode colocar um objeto grande na pilha, faça-o, porque é melhor. Há muitos motivos pelos quais você provavelmente não pode. Mas acho que ele está certo.
McKay
5

A única razão pela qual eu me preocuparia é que Dog agora está alocado na pilha, em vez de no heap. Então, se Dog tiver megabytes de tamanho, você pode ter um problema,

Se você precisar seguir a nova rota / excluir, tome cuidado com as exceções. E por causa disso, você deve usar auto_ptr ou um dos tipos de ponteiro inteligente boost para gerenciar o tempo de vida do objeto.

Roddy
fonte
1

Não há razão para novo (no heap) quando você pode alocar na pilha (a menos que por algum motivo você tenha uma pequena pilha e queira usar o heap.

Você pode querer considerar o uso de shared_ptr (ou uma de suas variantes) da biblioteca padrão se quiser alocar no heap. Isso cuidará da exclusão para você, uma vez que todas as referências ao shared_ptr tenham deixado de existir.

Scott Langham
fonte
Há muitas razões, por exemplo, veja-me a resposta.
perigosodave
Suponho que você queira que o objeto sobreviva ao escopo da função, mas o OP não parecia sugerir que era isso que eles estavam tentando fazer.
Scott Langham
0

Há um motivo adicional, que ninguém mais mencionou, por que você pode escolher criar seu objeto dinamicamente. Objetos dinâmicos baseados em heap permitem que você faça uso de polimorfismo .

perigosa
fonte
2
Você pode trabalhar com objetos baseados em pilha polimorficamente também, não vejo nenhuma diferença entre objetos alocados em pilha / pilha a esse respeito. Ex: void MyFunc () {Dog dog; Alimentar (cão); } void Feed (Animal & animal) {auto food = animal-> GetFavouriteFood (); PutFoodInBowl (comida); } // Neste exemplo GetFavouriteFood pode ser uma função virtual que cada animal substitui com sua própria implementação. Isso funcionará polimorficamente sem nenhum heap envolvido.
Scott Langham
-1: polimorfismo requer apenas uma referência ou ponteiro; é totalmente agnóstico quanto à duração do armazenamento da instância subjacente.
sublinhado_d
@underscore_d "Usar uma classe base universal implica em custo: os objetos devem ser alocados no heap para serem polimórficos;" - bjarne stroustrup stroustrup.com/bs_faq2.html#object
dangerousdave
LOL, não me importo se o próprio Stroustrup disse isso, mas essa é uma frase incrivelmente embaraçosa para ele, porque ele está errado. Não é difícil testar isso sozinho, você sabe: instancie alguns DerivedA, DerivedB e DerivedC na pilha; instancie também um ponteiro alocado na pilha para a Base e confirme se você pode encaixá-lo com A / B / C e usá-los polimorficamente. Aplique pensamento crítico e referência padrão às reivindicações, mesmo se forem do autor original do idioma. Aqui: stackoverflow.com/q/5894775/2757035
underscore_d
Colocando desta forma, eu tenho um objeto contendo 2 famílias separadas de quase 1000 objetos polimórficos cada, com duração de armazenamento automática. Eu instancio esse objeto na pilha e, ao referir-me a esses membros por meio de referências ou ponteiros, ele atinge todos os recursos de polimorfismo sobre eles. Nada em meu programa é alocado dinamicamente / em pilha (além dos recursos pertencentes a classes alocadas em pilha) e isso não altera as capacidades de meu objeto em um iota.
underscore_d
-4

Eu tive o mesmo problema no Visual Studio. Você tem que usar:

yourClass-> classMethod ();

ao invés de:

yourClass.classMethod ();

Hamed
fonte
3
Isso não responde à pergunta. Em vez disso, observe que é necessário usar uma sintaxe diferente para acessar o objeto alocado na pilha (por meio do ponteiro para o objeto) e o objeto alocado na pilha.
Alexey Ivanov