¿Cuándo debo usar la nueva palabra clave en C ++?

273

He estado usando C ++ por un tiempo corto, y me he estado preguntando sobre el nuevo palabra clave. Simplemente, ¿debería usarlo o no?

1) Con la nueva palabra clave ...

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

2) Sin la nueva palabra clave ...

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

Desde la perspectiva de la implementación, no parecen tan diferentes (pero estoy seguro de que lo son) ... Sin embargo, mi lenguaje principal es C # y, por supuesto, el primer método es a lo que estoy acostumbrado.

La dificultad parece ser que el método 1 es más difícil de usar con las clases estándar de C ++.

¿Qué método debo usar?

Actualización 1:

Recientemente utilicé la nueva palabra clave para memoria de almacenamiento dinámico (o tienda gratuita ) para una gran matriz que estaba fuera de alcance (es decir, que se devolvía de una función). Donde antes estaba usando la pila, lo que causaba que la mitad de los elementos se corrompiera fuera del alcance, el cambio al uso del montón aseguró que los elementos estuvieran intactos. ¡Hurra!

Actualización 2:

Un amigo mío me dijo recientemente que hay una regla simple para usar la newpalabra clave; cada vez que escribe new, escriba delete.

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

Esto ayuda a evitar pérdidas de memoria, ya que siempre debe colocar la eliminación en algún lugar (es decir, cuando la corta y pega en un destructor o no).

Nick Bolton
fuente
66
La respuesta corta es, use la versión corta cuando pueda salirse con la suya. :)
jalf
11
Una técnica mejor que escribir siempre una eliminación correspondiente: use contenedores STL y punteros inteligentes como std::vectory std::shared_ptr. Estos envuelven las llamadas hacia newy deletepara usted, por lo que es aún menos probable que pierda memoria. Pregúntese, por ejemplo: ¿siempre recuerda poner un correspondiente en deletetodas partes donde podría lanzarse una excepción? Poner deletes a mano es más difícil de lo que piensas.
AshleysBrain
@nbolton Re: ACTUALIZACIÓN 1: una de las cosas hermosas de C ++ es que le permite almacenar tipos definidos por el usuario en la pila, mientras que los archivos recolectados de basura como C # lo obligan a almacenar los datos en el montón . El almacenamiento de datos en el montón consume más recursos que el almacenamiento de datos en la pila , por lo tanto, debe preferir la pila al montón , excepto cuando su UDT requiere una gran cantidad de memoria para almacenar sus datos. (Esto también significa que los objetos se pasan por valor de forma predeterminada). Una mejor solución a su problema sería pasar la matriz a la función por referencia .
Charles Addis

Respuestas:

304

Método 1 (usando new)

  • Asigna memoria para el objeto en el tienda gratuita (esto suele ser lo mismo que el montón )
  • Requiere que explícitamente delete su objeto más tarde. (Si no lo elimina, podría crear una pérdida de memoria)
  • La memoria se mantiene asignada hasta que deletelo hagas . (es decir, podría returncrear un objeto que haya creado usandonew )
  • El ejemplo en la pregunta perderá memoria a menos que el puntero sea deleted; y siempre debe eliminarse , independientemente de qué ruta de control se tome, o si se lanzan excepciones.

Método 2 (sin usar new)

  • Asigna memoria para el objeto en la pila (donde van todas las variables locales) Generalmente hay menos memoria disponible para la pila; Si asigna demasiados objetos, corre el riesgo de desbordamiento de pila.
  • No lo necesitarás deletemás tarde.
  • La memoria ya no se asigna cuando sale del alcance. (es decir, no debe returnapuntar a un objeto en la pila)

En cuanto a cuál usar; elige el método que mejor funcione para usted, dadas las restricciones anteriores.

Algunos casos fáciles:

  • Si no desea preocuparse por las llamadas delete(y el potencial de causar pérdidas de memoria ), no debe usarlas new.
  • Si desea devolver un puntero a su objeto desde una función, debe usar new
Daniel LeCheminant
fuente
44
Un punto crítico: creo que el nuevo operador asigna memoria de "tienda libre", mientras que malloc asigna de "montón". No se garantiza que sean lo mismo, aunque en la práctica generalmente lo son. Ver gotw.ca/gotw/009.htm .
Fred Larson
44
Creo que su respuesta podría ser más clara para usar. (99% de las veces, la elección es simple. Use el método 2, en un objeto contenedor que llame a new / delete en constructor / destructor)
jalf
44
@jalf: el Método 2 es el que no usa el nuevo: - / En cualquier caso, hay muchas veces que el código será mucho más simple (por ejemplo, manejo de casos de error) usando el Método 2 (el que no tiene el nuevo)
Daniel LeCheminant
Otro punto crítico ... Deberías hacer más obvio que el primer ejemplo de Nick pierde memoria, mientras que el segundo no, incluso ante las excepciones.
Arafangion
44
@Fred, Arafangion: Gracias por tu perspicacia; He incorporado tus comentarios en la respuesta.
Daniel LeCheminant
118

Hay una diferencia importante entre los dos.

Todo lo que no está asignado se newcomporta de manera muy similar a los tipos de valor en C # (y la gente suele decir que esos objetos están asignados en la pila, que es probablemente el caso más común / obvio, pero no siempre es cierto. Más precisamente, los objetos asignados sin usar newtienen almacenamiento automático duración Todo asignado connew se asigna en el montón y se devuelve un puntero al mismo, exactamente como los tipos de referencia en C #.

Todo lo asignado en la pila debe tener un tamaño constante, determinado en tiempo de compilación (el compilador debe establecer el puntero de la pila correctamente, o si el objeto es miembro de otra clase, tiene que ajustar el tamaño de esa otra clase) . Es por eso que las matrices en C # son tipos de referencia. Tienen que serlo, porque con los tipos de referencia, podemos decidir en tiempo de ejecución cuánta memoria pedir. Y lo mismo se aplica aquí. Solo las matrices con tamaño constante (un tamaño que se puede determinar en tiempo de compilación) se pueden asignar con una duración de almacenamiento automática (en la pila). Las matrices de tamaño dinámico deben asignarse en el montón, llamando new.

(Y ahí es donde se detiene cualquier similitud con C #)

Ahora, cualquier cosa asignada en la pila tiene una duración de almacenamiento "automática" (en realidad puede declarar una variable como auto , pero este es el valor predeterminado si no se especifica ningún otro tipo de almacenamiento, por lo que la palabra clave no se usa realmente en la práctica, pero aquí es donde viene de)

La duración del almacenamiento automático significa exactamente cómo suena, la duración de la variable se maneja automáticamente. Por el contrario, cualquier cosa asignada en el montón debe ser eliminada manualmente por usted. Aquí hay un ejemplo:

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

Esta función crea tres valores que vale la pena considerar:

En la línea 1, declara una variable bde tipo baren la pila (duración automática).

En la línea 2, declara un barpuntero b2en la pila (duración automática) y llama a nuevo, asignando unbar objeto en el montón. (duración dinámica)

Cuando la función regrese, sucederá lo siguiente: Primero, b2queda fuera de alcance (el orden de destrucción siempre es opuesto al orden de construcción). Pero b2es solo un puntero, por lo que no pasa nada, la memoria que ocupa simplemente se libera. Y lo más importante, la memoria a la que apunta (la barinstancia en el montón) NO se toca. Solo se libera el puntero, porque solo el puntero tenía una duración automática. En segundo lugar, bqueda fuera de alcance, por lo que, dado que tiene una duración automática, se llama a su destructor y se libera la memoria.

Y el bar instancia en el montón? Probablemente todavía esté allí. Nadie se molestó en eliminarlo, por lo que hemos perdido memoria.

A partir de este ejemplo, podemos ver que cualquier cosa con duración automática tiene garantizado que se llamará a su destructor cuando salga del alcance. Eso es útil Pero cualquier cosa asignada en el almacenamiento dinámico dura tanto como lo necesitemos, y puede dimensionarse dinámicamente, como en el caso de las matrices. Eso también es útil. Podemos usar eso para administrar nuestras asignaciones de memoria. ¿Qué pasa si la clase Foo asignó algo de memoria en el montón en su constructor y eliminó esa memoria en su destructor? Entonces podríamos obtener lo mejor de ambos mundos, asignaciones de memoria seguras que se garantiza que serán liberadas nuevamente, pero sin las limitaciones de obligar a que todo esté en la pila.

Y eso es casi exactamente cómo funciona la mayoría del código C ++. Mira la biblioteca estándarstd::vector por ejemplo. Eso normalmente se asigna en la pila, pero se puede dimensionar y cambiar de tamaño dinámicamente. Y lo hace mediante la asignación interna de memoria en el montón según sea necesario. El usuario de la clase nunca ve esto, por lo que no hay posibilidad de pérdida de memoria u olvido de limpiar lo que asignó.

Este principio se llama RAII (Adquisición de recursos es inicialización) y se puede extender a cualquier recurso que deba adquirirse y liberarse. (tomas de red, archivos, conexiones de bases de datos, bloqueos de sincronización). Todos ellos pueden adquirirse en el constructor y liberarse en el destructor, por lo que tiene la garantía de que todos los recursos que adquiera se liberarán nuevamente.

Como regla general, nunca use new / delete directamente de su código de alto nivel. Siempre envuélvala en una clase que pueda administrar la memoria por usted y que garantice que se libere nuevamente. (Sí, puede haber excepciones a esta regla. En particular, los punteros inteligentes requieren que llame newdirectamente y pase el puntero a su constructor, que luego se hace cargo y asegura que deletese llame correctamente. Pero esta sigue siendo una regla general muy importante )

jalf
fuente
2
"Todo lo que no se asigna con nuevo se coloca en la pila" No en los sistemas en los que he trabajado ... generalmente los datos globales (estáticos) inicializados (y no iniciados) se colocan en sus propios segmentos. Por ejemplo, segmentos de enlace .data, .bss, etc. Pedantic, lo sé ...
Dan
Por supuesto que tienes razón. Realmente no estaba pensando en datos estáticos. Mi mal, por supuesto. :)
jalf
2
¿Por qué cualquier cosa asignada en la pila tiene que tener un tamaño constante?
user541686
No siempre es así , hay algunas formas de eludirlo, pero en el caso general lo hace, porque está en una pila. Si está en la parte superior de la pila, entonces es posible cambiar su tamaño, pero una vez que se empuja algo más encima, está "amurallado", rodeado de objetos a cada lado, por lo que no se puede cambiar su tamaño. . Sí, diciendo que siempre tiene que tener un tamaño fijo es un poco de una simplificación, pero transmite la idea básica (y que no recomendaría perder el tiempo con las funciones de C que le permiten ser demasiado creativo con asignaciones de pila)
JALF
14

¿Qué método debo usar?

Esto casi nunca está determinado por sus preferencias de escritura, sino por el contexto. Si necesita mantener el objeto en varias pilas o si es demasiado pesado para la pila, lo asigna en la tienda gratuita. Además, dado que está asignando un objeto, también es responsable de liberar la memoria. Buscar eldelete operador.

Para aliviar la carga del uso de la gestión de la tienda libre, la gente ha inventado cosas como auto_ptry unique_ptr. Le recomiendo que eche un vistazo a estos. Incluso podrían ser de ayuda para sus problemas de escritura ;-)

Dirkgently
fuente
10

Si está escribiendo en C ++, probablemente esté escribiendo para el rendimiento. Usar new y la tienda gratuita es mucho más lento que usar la pila (especialmente cuando se usan hilos), así que solo úselo cuando lo necesite.

Como han dicho otros, necesita nuevo cuando su objeto necesita vivir fuera de la función o del alcance del objeto, el objeto es realmente grande o cuando no conoce el tamaño de una matriz en tiempo de compilación.

Además, trate de evitar el uso de eliminar. Envuelva su nuevo en un puntero inteligente en su lugar. Deje que la llamada del puntero inteligente se elimine por usted.

Hay algunos casos en los que un puntero inteligente no es inteligente. Nunca almacene std :: auto_ptr <> dentro de un contenedor STL. Eliminará el puntero demasiado pronto debido a las operaciones de copia dentro del contenedor. Otro caso es cuando tienes un contenedor STL realmente grande de punteros a objetos. boost :: shared_ptr <> tendrá una tonelada de sobrecarga de velocidad ya que sube y baja los recuentos de referencia. La mejor manera de hacerlo en ese caso es colocar el contenedor STL en otro objeto y darle a ese objeto un destructor que invoque a delete en cada puntero en el contenedor.

Zan Lynx
fuente
10

La respuesta corta es: si eres un principiante en C ++, nunca deberías usar newodelete ti mismo.

En su lugar, debe usar punteros inteligentes como std::unique_ptry std::make_unique(o con menos frecuencia, std::shared_ptry std::make_shared). De esa manera, no tiene que preocuparse tanto por las pérdidas de memoria. E incluso si es más avanzado, la mejor práctica generalmente sería encapsular la forma personalizada que está utilizando newy deleteen una clase pequeña (como un puntero inteligente personalizado) que se dedica solo a los problemas del ciclo de vida del objeto.

Por supuesto, detrás de escena, estos punteros inteligentes siguen realizando asignaciones dinámicas y desasignaciones, por lo que el código que los use aún tendría la sobrecarga de tiempo de ejecución asociada. Otras respuestas aquí han cubierto estos problemas, y cómo tomar decisiones de diseño sobre cuándo usar punteros inteligentes en lugar de simplemente crear objetos en la pila o incorporarlos como miembros directos de un objeto, lo suficientemente bien como para no repetirlos. Pero mi resumen ejecutivo sería: no use punteros inteligentes o asignación dinámica hasta que algo lo obligue a hacerlo.

Daniel Schepler
fuente
interesante ver cómo puede cambiar una respuesta a medida que pasa el tiempo;)
Wolf
2

La respuesta simple es sí: new () crea un objeto en el montón (con el desafortunado efecto secundario de que tiene que administrar su vida útil (llamando explícitamente a delete en él), mientras que la segunda forma crea un objeto en la pila en el actual alcance y ese objeto será destruido cuando salga del alcance.

Timo Geusch
fuente
1

Si su variable se usa solo dentro del contexto de una sola función, es mejor usar una variable de pila, es decir, la Opción 2. Como han dicho otros, no tiene que administrar la vida útil de las variables de pila: se construyen y destruido automáticamente Además, la asignación / desasignación de una variable en el montón es lenta en comparación. Si su función se llama con la frecuencia suficiente, verá una tremenda mejora en el rendimiento si usa variables de pila versus variables de pila.

Dicho esto, hay un par de casos obvios en los que las variables de la pila son insuficientes.

Si la variable de la pila tiene una gran huella de memoria, corre el riesgo de desbordar la pila. Por defecto, el tamaño de la pila de cada hilo es de 1 MB en Windows. Es poco probable que cree una variable de pila que tenga un tamaño de 1 MB, pero debe tener en cuenta que la utilización de la pila es acumulativa. Si su función llama a una función que llama a otra función que llama a otra función que ..., las variables de la pila en todas estas funciones ocupan espacio en la misma pila. Las funciones recursivas pueden encontrarse con este problema rápidamente, dependiendo de qué tan profunda sea la recursividad. Si esto es un problema, puede aumentar el tamaño de la pila (no recomendado) o asignar la variable en el montón utilizando el nuevo operador (recomendado).

La otra condición más probable es que su variable necesite "vivir" más allá del alcance de su función. En este caso, asignaría la variable en el montón para que pueda alcanzarse fuera del alcance de cualquier función dada.

Matt Davis
fuente
1

¿Está pasando myClass fuera de una función, o espera que exista fuera de esa función? Como algunos otros dijeron, se trata de alcance cuando no está asignando en el montón. Cuando abandonas la función, desaparece (eventualmente). Uno de los errores clásicos cometidos por los principiantes es el intento de crear un objeto local de alguna clase en una función y devolverlo sin asignarlo en el montón. Recuerdo haber depurado este tipo de cosas en mis primeros días haciendo c ++.

itsmatt
fuente
0

El segundo método crea la instancia en la pila, junto con cosas como algo declarado inty la lista de parámetros que se pasan a la función.

El primer método deja espacio para un puntero en la pila, que ha establecido en la ubicación en la memoria donde MyClassse ha asignado un nuevo en el montón, o tienda libre.

El primer método también requiere que crees deletelo que creasnew , mientras que en el segundo método, la clase se destruye y libera automáticamente cuando cae fuera del alcance (la siguiente llave de cierre, por lo general).

Greyfade
fuente
-1

La respuesta corta es sí, la palabra clave "nueva" es increíblemente importante ya que cuando la usa, los datos del objeto se almacenan en el montón en lugar de en la pila, ¡lo que es más importante!

RAGNO
fuente