2008-12-02 32 views
105

Soy un programador de C que intenta comprender C++. Muchos tutoriales demuestran instancias de objetos utilizando un fragmento tales como:Creación de objetos en C++

Dog* sparky = new Dog(); 

lo que implica que más adelante se va a hacer:

delete sparky; 

que tiene sentido. Ahora bien, en el caso de que la asignación de memoria dinámica es innecesaria, ¿hay alguna razón para utilizar lo anterior en lugar de

Dog sparky; 

y dejar que el destructor se llama una vez Sparky se sale del ámbito?

Gracias!

Respuesta

152

En la al contrario, siempre debe preferir las asignaciones de pila, en la medida en que, como regla general, nunca debe tener nuevo/eliminar en su código de usuario.

Como dices, cuando la variable se declara en la pila, se llama automáticamente a su destructor cuando se sale del alcance, que es tu herramienta principal para rastrear el tiempo de vida del recurso y evitar fugas.

Así que, en general, cada vez que se necesita para asignar un recurso, si se trata de la memoria (llamando nueva), identificadores de archivo, tomas de corriente o cualquier otra cosa, lo envuelve en una clase donde el constructor adquiere el recurso, y los comunicados de destructor eso. Luego puede crear un objeto de ese tipo en la pila, y se le garantiza que su recurso se liberará cuando salga del alcance. De esta manera, no tiene que seguir sus pares nuevos/eliminar en todas partes para asegurarse de evitar fugas de memoria.

El nombre más común para este idioma es RAII

también se ven en clases de punteros inteligentes que se utilizan para envolver los punteros resultantes en los raros casos en que usted tiene que asignar algo con nuevo fuera de un objeto RAII dedicado. En su lugar, pasa el puntero a un puntero inteligente, que luego rastrea su duración, por ejemplo, mediante recuento de referencias, y llama al destructor cuando la última referencia sale del alcance.La biblioteca estándar tiene std::unique_ptr para la administración simple basada en el alcance y std::shared_ptr que hace referencia al recuento para implementar la propiedad compartida.

Muchos tutoriales demuestran objeto de instancias utilizando un fragmento como ...

Así que lo que he descubierto es que la mayoría de los tutoriales son los peores. ;) La mayoría de los tutoriales le enseñan prácticas pésimas de C++, incluida la invocación de nuevas/eliminar para crear variables cuando no es necesario, y le dificultan el seguimiento de la vida de sus asignaciones.

+0

Los punteros sin formato son útiles cuando se quiere una semántica parecida a auto_ptr (transferencia de propiedad), pero se conserva la operación de intercambio sin lanzamiento y no se desea la sobrecarga del recuento de referencias. Edge case tal vez, pero útil. –

+2

Esta es una respuesta correcta, pero la razón por la que nunca tendría el hábito de crear objetos en la pila es porque nunca es completamente obvio qué tan grande será ese objeto. Solo está pidiendo una excepción de desbordamiento de pila. – dviljoen

+3

Greg: Ciertamente. Pero como dices, un caso extremo. En general, es mejor evitar punteros. Pero están en el idioma por una razón, sin negar eso. :) dviljoen: Si el objeto es grande, lo envuelve en un objeto RAII, que puede asignarse en la pila y contiene un puntero a los datos asignados al montón. – jalf

13

Bueno, la razón para usar el puntero sería exactamente la misma que la razón para usar punteros en C asignados con malloc: si desea que su objeto viva más que su variable.

Incluso se recomienda NO utilizar el nuevo operador si puede evitarlo. Especialmente si usas excepciones. En general, es mucho más seguro dejar que el compilador libere sus objetos.

5

La única razón por la que me preocuparía es que Dog ahora está asignado en la pila, en lugar de en el montón. Entonces, si Dog tiene un tamaño de megabytes, puede tener un problema,

Si necesita ir a la ruta nueva/eliminar, desconfíe de las excepciones. Y debido a esto, debe usar auto_ptr o uno de los tipos de puntero inteligente para administrar la vida útil del objeto.

1

No hay razón para nueva (en el montón) cuando se puede asignar en la pila (a menos que por alguna razón usted tiene una pequeña pila y desea utilizar el montón.

Es posible que desee considerar el uso de un shared_ptr (o una de sus variantes) de la biblioteca estándar si desea asignar en el montón. Esto se encargará de hacer la eliminación una vez que todas las referencias a shared_ptr hayan desaparecido.

+0

Hay muchas razones, por ejemplo, ver mi respuesta. – dangerousdave

+0

Supongo que es posible que desee que el objeto sobreviva al alcance de la función, pero el OP no parece sugerir que es lo que estaban tratando de hacer. –

7

Trate el montón como un bien inmueble muy importante y úselo con mucha prudencia. La regla básica es usar la pila siempre que sea posible y usar el montón cuando no haya otra forma. Al asignar los objetos en la pila puede obtener muchos beneficios tales como:

(1). No necesita preocuparse por el desenrollado de la pila en caso de excepciones

(2). No necesita preocuparse por la fragmentación de la memoria provocada por la asignación de más espacio del necesario por su administrador de montón.

+1

Debe haber alguna consideración en cuanto al tamaño del objeto. La pila tiene un tamaño limitado, por lo que los objetos muy grandes deben asignarse a Heap. – Adam

+1

@ Adam Creo que Naveen sigue siendo correcto aquí. Si puedes poner un objeto grande en la pila, hazlo, porque es mejor. Hay muchas razones por las que probablemente no puedas. Pero creo que está en lo cierto. – McKay

14

He visto este antipatrón de personas que no obtienen el operador de dirección &. Si necesitan llamar a una función con un puntero, siempre asignarán en el montón para que obtengan un puntero.

void FeedTheDog(Dog* hungryDog); 

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

Dog goodDog; 
FeedTheDog(&goodDog); 
+0

Alternativamente: void FeedTheDog (Dog & hungryDog); Perro perro; FeedTheDog (perro); –

+1

@ScottLangham cierto, pero ese no era el punto que estaba tratando de hacer. A veces llamas a una función que toma un puntero nulo opcional, por ejemplo, o tal vez es una API existente que no se puede cambiar. –

-4

Tuve el mismo problema en Visual Studio. Tienes que usar:

yourClass-> classMethod();

en lugar de:

yourClass.classMethod();

+2

Esto no responde la pregunta. Más bien se nota que uno tiene que usar una sintaxis diferente para acceder al objeto asignado en el montón (a través del puntero al objeto) y el objeto asignado en la pila. –

0

Hay una razón adicional, que nadie más ha mencionado, por qué puede elegir crear su objeto dinámicamente. Los objetos dinámicos basados ​​en el montón le permiten hacer uso de polymorphism.

+2

También puede trabajar con objetos basados ​​en pila polimórficamente, no veo ninguna diferencia entre los objetos asignados stack/y heap en este sentido. Ej: void MyFunc() {Perro de perro; Dientes de arrastre); } void Feed (animal y animal) {auto food = animal-> GetFavouriteFood(); PutFoodInBowl (comida); } // En este ejemplo, GetFavouriteFood puede ser una función virtual que cada animal anula con su propia implementación. Esto funcionará polimórficamente sin involucrar ningún montón. –

+0

-1: el polimorfismo solo requiere una referencia o puntero; es totalmente agnóstico de la duración de almacenamiento de la instancia subyacente. –

+0

@underscore_d "El uso de una clase base universal implica un costo: los objetos deben asignarse en forma de pila para que sean polimórficos;" - bjarne stroustrup http://www.stroustrup.com/bs_faq2.html#object – dangerousdave

19

Aunque tener cosas en la pila podría ser una ventaja en términos de asignación y liberación automática de cosas, tiene algunas desventajas.

  1. Es posible que no desee asignar grandes objetos a la pila.

  2. Tipos de tiempo de ejecución! Considere este código:

#include <iostream> 

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

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

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

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

Obviamente imprimirá "B \ n". Ahora vamos a ver lo que sucede cuando se utiliza la pila:

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

Será (C++ para chicos Obviosly) print "a \ n". Y esto puede no ser intuitivo para aquellos que son similares a Java u otros lenguajes de estilo OOP. La razón es que ya no tienes un puntero en una instancia de B. Pero se crea una instancia de B y luego se copia a una instancia de A (que está recién creada implícitamente).

Desafortunadamente este es uno de los grandes problemas de diseño de C++ en mi opinión. Muchas cosas suceden intuitivamente.En C tienes tus punteros y eso es todo. Usted sabe cómo usarlos y SIEMPRE son iguales. En C++, este no es el caso. Solo imagina lo que sucede, cuando usas un en este ejemplo como argumento para un método: las cosas se vuelven cada vez más complicadas y SIGNIFICA una gran diferencia si a es de tipo A o A * y si el método es llamado por -valor o call-by-reference (muchas combinaciones son posibles y [bastante] todas se comportan de otra manera).

+1

-1: las personas que no pueden comprender la semántica de valores y el simple hecho de que el polimorfismo necesita una referencia/puntero (para evitar el corte de objetos) no constituyen ningún tipo de problema con el lenguaje. El poder de C++ no debe considerarse un inconveniente solo porque algunas personas no puedan aprender sus reglas básicas. –

+0

Gracias por el aviso. Tuve un problema similar con la toma de método en objetos en lugar de puntero o referencia, y qué desastre fue. El objeto tiene punteros dentro y se eliminaron demasiado pronto debido a este enfrentamiento. – BoBoDev