Creación de instancias de objetos C ++

113

Soy un programador de C que intenta entender C ++. Muchos tutoriales demuestran la creación de instancias de objetos mediante un fragmento como:

Dog* sparky = new Dog();

lo que implica que más adelante harás:

delete sparky;

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

Dog sparky;

y dejar que se llame al destructor una vez que Sparky salga del alcance?

¡Gracias!

el_champo
fuente

Respuestas:

166

Por el 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 dice, cuando la variable se declara en la pila, su destructor se llama automáticamente cuando sale del alcance, que es su herramienta principal para rastrear la vida útil de los recursos y evitar fugas.

Entonces, en general, cada vez que necesite asignar un recurso, ya sea memoria (llamando a new), manejadores de archivos, sockets o cualquier otra cosa, envuélvalo en una clase donde el constructor adquiere el recurso y el destructor lo libera. Luego, puede crear un objeto de ese tipo en la pila y tiene la garantía de que su recurso se liberará cuando salga del alcance. De esa manera, no tiene que rastrear sus pares nuevos / eliminados en todas partes para asegurarse de evitar pérdidas de memoria.

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

También busque las clases de punteros inteligentes que se utilizan para ajustar los punteros resultantes en los raros casos en los que tiene que asignar algo nuevo fuera de un objeto RAII dedicado. En su lugar, pasa el puntero a un puntero inteligente, que luego rastrea su vida útil, por ejemplo, mediante el conteo de referencias, y llama al destructor cuando la última referencia sale del alcance. La biblioteca estándar tiene std::unique_ptruna gestión sencilla basada en el alcance y std::shared_ptrque hace referencia al recuento para implementar la propiedad compartida.

Muchos tutoriales demuestran la creación de instancias de objetos utilizando un fragmento como ...

Entonces, lo que has descubierto es que la mayoría de los tutoriales apestan. ;) La mayoría de los tutoriales le enseñan pésimas prácticas de C ++, incluida la llamada a new / delete para crear variables cuando no es necesario, y le dificulta el seguimiento de la vida útil de sus asignaciones.

jalf
fuente
Los punteros sin procesar son útiles cuando desea una semántica similar a auto_ptr (transferencia de propiedad), pero retiene la operación de intercambio sin lanzamiento y no desea la sobrecarga del recuento de referencias. Tal vez el caso Edge, pero útil.
Greg Rogers
2
Esta es una respuesta correcta, pero la razón por la que nunca me acostumbraría a crear objetos en la pila es porque nunca es completamente obvio qué tan grande será ese objeto. Solo está solicitando una excepción de desbordamiento de pila.
dviljoen
3
Greg: Ciertamente. Pero como dices, un caso extremo. En general, es mejor evitar los consejos. Pero están en el idioma por una razón, no se puede negar. :) dviljoen: Si el objeto es grande, lo envuelve en un objeto RAII, que se puede asignar en la pila, y contiene un puntero a los datos asignados al montón.
jalf
3
@dviljoen: No, no lo soy. Los compiladores de C ++ no inflan objetos innecesariamente. Lo peor que verá es que normalmente se redondea al múltiplo de cuatro bytes más cercano. Por lo general, una clase que contiene un puntero ocupará tanto espacio como el propio puntero, por lo que no le cuesta nada en el uso de la pila.
Jalf
20

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

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

  2. Despacho 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;
}

Esto imprimirá "B". Ahora veamos qué sucede cuando se usa Stack:

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

Esto imprimirá "A", que puede no ser intuitivo para aquellos que están familiarizados con Java u otros lenguajes orientados a objetos. La razón es que ya no tiene un puntero a una instancia de B. En su lugar, Bse crea una instancia de y se copia en la avariable de tipo A.

Algunas cosas pueden suceder de forma poco intuitiva, especialmente cuando es nuevo en C ++. En C tienes tus punteros y eso es todo. Sabes cómo usarlos y SIEMPRE hacen lo mismo. En C ++ este no es el caso. Imagínese lo que sucede, cuando se utiliza una en este ejemplo como un argumento a favor de un método - las cosas se complican y se hace una gran diferencia si aes de tipo Ao A*incluso A&(llamada por referencia). Son posibles muchas combinaciones y todas se comportan de manera diferente.

Universo
fuente
2
-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 constituye ningún tipo de problema con el lenguaje. El poder de c ++ no debe considerarse un inconveniente solo porque algunas personas no pueden aprender sus reglas básicas.
underscore_d
Gracias por el aviso. Tuve un problema similar con el método que toma el objeto en lugar del puntero o la referencia, y qué lío fue. El objeto tiene punteros en su interior y se eliminaron demasiado pronto debido a esta adaptación.
BoBoDev
@underscore_d Estoy de acuerdo; el último párrafo de esta respuesta debe eliminarse. No tome el hecho de que edité esta respuesta para significar que estoy de acuerdo con ella; Simplemente no quería que se leyeran los errores.
TamaMcGlinn
@TamaMcGlinn estuvo de acuerdo. Gracias por la buena edición. Eliminé la parte de opinión.
UNIVERSIDAD
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 quieres que tu objeto viva más tiempo que tu variable!

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

PierreBdR
fuente
13

He visto este anti-patrón en personas que no obtienen el operador & address-of. 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);
Mark Ransom
fuente
Alternativamente: void FeedTheDog (Dog & hungryDog); Perro perro; FeedTheDog (perro);
Scott Langham
1
@ScottLangham es cierto, pero ese no era el punto que estaba tratando de hacer. A veces, está llamando a una función que toma un puntero NULL opcional, por ejemplo, o tal vez es una API existente que no se puede cambiar.
Mark Ransom
7

Trate el montón como un bien inmueble muy importante y utilícelo con mucho cuidado. La regla básica es usar stack siempre que sea posible y usar heap siempre que no haya otra forma. Al asignar los objetos en la pila, puede obtener muchos beneficios, como:

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

(2). No necesita preocuparse por la fragmentación de la memoria causada por la asignación de más espacio del necesario por parte de su administrador de almacenamiento dinámico.

Naveen
fuente
1
Debe tenerse en cuenta el tamaño del objeto. La pila tiene un tamaño limitado, por lo que los objetos muy grandes deben asignarse al montón.
Adam
1
@Adam Creo que Naveen todavía tiene razón aquí. Si puede poner un objeto grande en la pila, hágalo, porque es mejor. Hay muchas razones por las que probablemente no pueda. Pero creo que tiene razón.
McKay
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, es posible que tenga un problema,

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

Roddy
fuente
1

No hay razón para ser nuevo (en la pila) cuando puede asignar en la pila (a menos que por alguna razón tenga una pila pequeña y quiera usar la pila.

Es posible que desee considerar el uso de shared_ptr (o una de sus variantes) de la biblioteca estándar si desea realizar una asignación en el montón. Eso se encargará de hacer la eliminación una vez que todas las referencias al shared_ptr hayan dejado de existir.

Scott Langham
fuente
Hay muchas razones, por ejemplo, véanme responder.
dangerousdave el
Supongo que es posible que desee que el objeto sobreviva al alcance de la función, pero el OP no parecía sugerir que eso era lo que estaban tratando de hacer.
Scott Langham
0

Hay una razón adicional, que nadie más ha mencionado, por la que puede optar por crear su objeto de forma dinámica. Los objetos dinámicos basados ​​en montones le permiten hacer uso del polimorfismo .

peligroso
fuente
2
También puede trabajar con objetos basados ​​en pilas de forma polimórfica, no veo ninguna diferencia entre los objetos asignados a pila y al montón a este respecto. Por ejemplo: void MyFunc () {Perro perro; Dientes de arrastre); } void Feed (Animal & 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á de forma polimórfica sin ningún montón involucrado.
Scott Langham
-1: el polimorfismo requiere solo una referencia o puntero; es totalmente independiente de la duración del almacenamiento de la instancia subyacente.
underscore_d
@underscore_d "El uso de una clase base universal implica un costo: los objetos deben asignarse en montón para que sean polimórficos;" - bjarne stroustrup stroustrup.com/bs_faq2.html#object
dangerousdave
LOL, no me importa si el mismo Stroustrup lo dijo, pero esa es una cita increíblemente vergonzosa para él, porque está equivocado. No es difícil probar esto usted mismo, ¿sabe? Cree una instancia de DerivedA, DerivedB y DerivedC en la pila; Cree también una instancia de un puntero asignado a la pila a Base y confirme que puede colocarlo con A / B / C y usarlos polimórficamente. Aplicar el pensamiento crítico y la referencia estándar a las afirmaciones, incluso si son del autor original del idioma. Aquí: stackoverflow.com/q/5894775/2757035
underscore_d
Dicho de esta manera, tengo un objeto que contiene 2 familias separadas de casi 1000 cada uno de los objetos polimórficos, con una duración de almacenamiento automático. Instalo este objeto en la pila y, al referirme a esos miembros a través de referencias o punteros, obtiene todas las capacidades de polimorfismo sobre ellos. Nada en mi programa se asigna dinámicamente / en montón (aparte de los recursos que pertenecen a las clases asignadas en la pila), y no cambia las capacidades de mi objeto ni un ápice.
underscore_d
-4

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

yourClass-> classMethod ();

más bien que:

yourClass.classMethod ();

Hamed
fuente
3
Esto no responde a la pregunta. Prefiere notar 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 al objeto asignado en la pila.
Alexey Ivanov