¿Por qué los programadores de C ++ deberían minimizar el uso de 'nuevo'?


873

Me topé con la pregunta de desbordamiento de pila Pérdida de memoria con std :: string al usar std :: list <std :: string> , y uno de los comentarios dice esto:

Deja de usar newtanto. No veo ninguna razón por la que usaste nuevo en cualquier lugar que lo hiciste. Puede crear objetos por valor en C ++ y es una de las grandes ventajas de usar el lenguaje.
No tiene que asignar todo en el montón.
Deja de pensar como un programador de Java .

No estoy muy seguro de qué quiere decir con eso.

¿Por qué los objetos deben crearse por valor en C ++ tan a menudo como sea posible, y qué diferencia hace internamente?
¿Interpreté mal la respuesta?

Respuestas:


1037

Hay dos técnicas de asignación de memoria ampliamente utilizadas: asignación automática y asignación dinámica. Comúnmente, hay una región de memoria correspondiente para cada uno: la pila y el montón.

Apilar

La pila siempre asigna memoria de forma secuencial. Puede hacerlo porque requiere que libere la memoria en el orden inverso (Primero en entrar, Último en salir: FILO). Esta es la técnica de asignación de memoria para variables locales en muchos lenguajes de programación. Es muy, muy rápido porque requiere una contabilidad mínima y la siguiente dirección para asignar está implícita.

En C ++, esto se llama almacenamiento automático porque el almacenamiento se reclama automáticamente al final del alcance. Tan pronto como se completa la ejecución del bloque de código actual (delimitado usando {}), la memoria para todas las variables en ese bloque se recopila automáticamente. Este es también el momento en que se invocan los destructores para limpiar los recursos.

Montón

El montón permite un modo de asignación de memoria más flexible. La contabilidad es más compleja y la asignación es más lenta. Como no hay un punto de liberación implícito, debe liberar la memoria manualmente, usando deleteo delete[]( freeen C). Sin embargo, la ausencia de un punto de liberación implícito es la clave para la flexibilidad del montón.

Razones para usar la asignación dinámica

Incluso si el uso del montón es más lento y potencialmente conduce a pérdidas de memoria o fragmentación de la memoria, existen casos de uso perfectamente buenos para la asignación dinámica, ya que es menos limitado.

Dos razones clave para usar la asignación dinámica:

  • No sabe cuánta memoria necesita en tiempo de compilación. Por ejemplo, cuando lee un archivo de texto en una cadena, generalmente no sabe qué tamaño tiene el archivo, por lo que no puede decidir cuánta memoria asignar hasta que ejecute el programa.

  • Desea asignar memoria que persistirá después de abandonar el bloque actual. Por ejemplo, es posible que desee escribir una función string readfile(string path)que devuelva el contenido de un archivo. En este caso, incluso si la pila pudiera contener todo el contenido del archivo, no podría regresar de una función y mantener el bloque de memoria asignado.

Por qué la asignación dinámica a menudo es innecesaria

En C ++ hay una construcción ordenada llamada destructor . Este mecanismo le permite administrar recursos alineando la vida útil del recurso con la vida útil de una variable. Esta técnica se llama RAII y es el punto distintivo de C ++. "Envuelve" recursos en objetos. std::stringEs un ejemplo perfecto. Este fragmento:

int main ( int argc, char* argv[] )
{
    std::string program(argv[0]);
}

en realidad asigna una cantidad variable de memoria. El std::stringobjeto asigna memoria usando el montón y lo libera en su destructor. En este caso, no necesitaba administrar manualmente ningún recurso y aún así obtuvo los beneficios de la asignación dinámica de memoria.

En particular, implica que en este fragmento:

int main ( int argc, char* argv[] )
{
    std::string * program = new std::string(argv[0]);  // Bad!
    delete program;
}

Hay asignación de memoria dinámica innecesaria. El programa requiere más tipeo (!) E introduce el riesgo de olvidarse de desasignar la memoria. Lo hace sin ningún beneficio aparente.

¿Por qué debería usar el almacenamiento automático tan a menudo como sea posible?

Básicamente, el último párrafo lo resume. El uso del almacenamiento automático con la mayor frecuencia posible hace que sus programas:

  • más rápido de escribir;
  • más rápido cuando corres;
  • menos propenso a pérdidas de memoria / recursos.

Puntos extra

En la pregunta referenciada, hay preocupaciones adicionales. En particular, la siguiente clase:

class Line {
public:
    Line();
    ~Line();
    std::string* mString;
};

Line::Line() {
    mString = new std::string("foo_bar");
}

Line::~Line() {
    delete mString;
}

En realidad, es mucho más riesgoso de usar que el siguiente:

class Line {
public:
    Line();
    std::string mString;
};

Line::Line() {
    mString = "foo_bar";
    // note: there is a cleaner way to write this.
}

La razón es que std::stringdefine correctamente un constructor de copia. Considere el siguiente programa:

int main ()
{
    Line l1;
    Line l2 = l1;
}

Al usar la versión original, este programa probablemente se bloqueará, ya que se usa deleteen la misma cadena dos veces. Usando la versión modificada, cada Lineinstancia será propietaria de su propia instancia de cadena , cada una con su propia memoria y ambas serán lanzadas al final del programa.

Otras notas

El uso extensivo de RAII se considera una mejor práctica en C ++ debido a todas las razones anteriores. Sin embargo, hay un beneficio adicional que no es inmediatamente obvio. Básicamente, es mejor que la suma de sus partes. Todo el mecanismo compone . Se escala.

Si usa la Lineclase como un bloque de construcción:

 class Table
 {
      Line borders[4];
 };

Entonces

 int main ()
 {
     Table table;
 }

asigna cuatro std::stringinstancias, cuatro Lineinstancias, una Tableinstancia y todos los contenidos de la cadena y todo se libera automáticamente .


55
+1 por mencionar RAII al final, pero debería haber algo sobre las excepciones y el desbobinado de la pila.
Tobu

77
@Tobu: sí, pero esta publicación ya es bastante larga, y quería mantenerla bastante centrada en la pregunta de OP. Terminaré escribiendo una publicación de blog o algo así y enlazaré desde aquí.
André Caron

15
Sería un gran complemento mencionar el inconveniente de la asignación de la pila (al menos hasta C ++ 1x): a menudo necesita copiar cosas innecesariamente si no tiene cuidado. Por ejemplo, a Monsterescupe Treasurea Worldcuando muere. En su Die()método, agrega el tesoro al mundo. Debe usarse world->Add(new Treasure(/*...*/))en otro para preservar el tesoro después de que muera. Las alternativas son shared_ptr(pueden ser excesivas), auto_ptr(semántica deficiente para la transferencia de propiedad), pasar por valor (derrochador) y move+ unique_ptr(aún no se ha implementado ampliamente).
kizzx2

77
Lo que usted dijo sobre las variables locales asignadas a la pila puede ser un poco engañoso. "La pila" se refiere a la pila de llamadas, que almacena los marcos de la pila . Son estos marcos de pila que se almacenan en forma LIFO. Las variables locales para un marco específico se asignan como si fueran miembros de una estructura.
someguy

77
@someguy: De hecho, la explicación no es perfecta. La implementación tiene libertad en su política de asignación. Sin embargo, se requiere que las variables se inicialicen y destruyan de forma LIFO, por lo que la analogía se mantiene. No creo que sea un trabajo que complica aún más la respuesta.
André Caron

171

Porque la pila es más rápida y a prueba de fugas

En C ++, se necesita una sola instrucción para asignar espacio, en la pila, para cada objeto de ámbito local en una función determinada, y es imposible perder nada de esa memoria. Ese comentario pretendía (o debería haber pretendido) decir algo como "usar la pila y no el montón".


18
"se necesita una sola instrucción para asignar espacio" - oh, sin sentido. Claro que solo se necesita una instrucción para agregar al puntero de la pila, pero si la clase tiene alguna estructura interna interesante, habrá mucho más que agregar al puntero de la pila. Es igualmente válido decir que en Java no se necesitan instrucciones para asignar espacio, porque el compilador administrará las referencias en tiempo de compilación.
Charlie Martin

31
@Charlie está en lo correcto. Las variables automáticas son rápidas y a toda prueba sería más preciso.
Oliver Charlesworth

28
@Charlie: Las partes internas de la clase deben configurarse de cualquier manera. La comparación se está haciendo para asignar el espacio requerido.
Oliver Charlesworth

51
tos int x; return &x;
peterchen

16
rápido si. Pero ciertamente no es infalible. Nada es infalible. Puede obtener un StackOverflow :)
rxantos

107

El motivo es complicado.

Primero, C ++ no es basura recolectada. Por lo tanto, para cada nuevo, debe haber una eliminación correspondiente. Si no puede poner esta eliminación, entonces tiene una pérdida de memoria. Ahora, para un caso simple como este:

std::string *someString = new std::string(...);
//Do stuff
delete someString;

Esto es simple. Pero, ¿qué sucede si "Hacer cosas" arroja una excepción? Vaya: pérdida de memoria. ¿Qué sucede si "Do stuff" emite returntemprano? Vaya: pérdida de memoria.

Y esto es para el caso más simple . Si le devuelve esa cadena a alguien, ahora tiene que eliminarla. Y si lo pasan como argumento, ¿la persona que lo recibe necesita eliminarlo? ¿Cuándo deberían eliminarlo?

O simplemente puedes hacer esto:

std::string someString(...);
//Do stuff

No se delete. El objeto se creó en la "pila" y se destruirá una vez que salga del alcance. Incluso puede devolver el objeto, transfiriendo así su contenido a la función de llamada. Puede pasar el objeto a funciones (normalmente como referencia o referencia constante:. void SomeFunc(std::string &iCanModifyThis, const std::string &iCantModifyThis)Y así sucesivamente).

Todo sin newy delete. No se trata de quién posee la memoria o quién es responsable de eliminarla. Si lo haces:

std::string someString(...);
std::string otherString;
otherString = someString;

Se entiende que otherStringtiene una copia de los datos de someString. No es un puntero; Es un objeto separado. Puede que tengan el mismo contenido, pero puede cambiar uno sin afectar al otro:

someString += "More text.";
if(otherString == someString) { /*Will never get here */ }

¿Ves la idea?


1
En esa nota ... Si un objeto se asigna dinámicamente main(), existe durante la duración del programa, no se puede crear fácilmente en la pila debido a la situación, y los punteros a él se pasan a las funciones que requieren acceso a él. , ¿puede esto causar una fuga en el caso de un bloqueo del programa, o sería seguro? Asumiría lo último, ya que el SO que asigna toda la memoria del programa también debería desasignarlo lógicamente, pero no quiero asumir nada cuando se trata de eso new.
Justin Time - Restablece a Monica

44
@JustinTime No necesita preocuparse por liberar memoria de objetos asignados dinámicamente que permanecerán durante la vida útil del programa. Cuando se ejecuta un programa, el sistema operativo crea un atlas de memoria física, o memoria virtual, para ello. Cada dirección en el espacio de memoria virtual se asigna a una dirección de memoria física, y cuando el programa sale, todo lo que se asigna a su memoria virtual se libera. Por lo tanto, siempre que el programa se cierre por completo, no debe preocuparse por si la memoria asignada nunca se elimina.
Aiman ​​Al-Eryani

75

Los objetos creados por newdeben ser eventualmente deleteno sea que tengan fugas. No se llamará al destructor, no se liberará memoria, todo el bit. Como C ++ no tiene recolección de basura, es un problema.

Los objetos creados por valor (es decir, en la pila) mueren automáticamente cuando salen del alcance. El compilador inserta la llamada del destructor y la memoria se libera automáticamente al regresar la función.

Los punteros inteligentes unique_ptr, como , shared_ptrresuelven el problema de referencia pendiente, pero requieren disciplina de codificación y tienen otros problemas potenciales (copiabilidad, bucles de referencia, etc.).

Además, en escenarios muy multiproceso, newes un punto de contención entre hilos; Puede haber un impacto en el rendimiento por el uso excesivo new. La creación de objetos de pila es por definición thread-local, ya que cada hilo tiene su propia pila.

La desventaja de los objetos de valor es que mueren una vez que la función del host regresa: no puede pasar una referencia a aquellos al llamador, solo copiando, devolviendo o moviendo por valor.


99
+1. Re "Los objetos creados por newdeben ser eventualmente deleteno sea que se filtren". - peor aún, new[]debe coincidir con delete[], y obtendrá un comportamiento indefinido si delete new[]-edió la memoria o delete[] new-edió la memoria - muy pocos compiladores advierten sobre esto (algunas herramientas como Cppcheck lo hacen cuando pueden).
Tony Delroy

3
@TonyDelroy Hay situaciones en las que el compilador no puede advertir esto. Si una función devuelve un puntero, podría crearse si es nueva (un solo elemento) o nueva [].
fbafelipe

32
  • C ++ no emplea ningún administrador de memoria por sí solo. Otros lenguajes como C #, Java tiene recolector de basura para manejar la memoria
  • Las implementaciones de C ++ generalmente usan rutinas del sistema operativo para asignar la memoria y demasiada nueva / eliminación podría fragmentar la memoria disponible
  • Con cualquier aplicación, si la memoria se usa con frecuencia, es aconsejable preasignarla y liberarla cuando no sea necesario.
  • La gestión incorrecta de la memoria podría provocar pérdidas de memoria y es realmente difícil de rastrear. Por lo tanto, usar objetos de pila dentro del alcance de la función es una técnica probada
  • La desventaja de usar objetos de pila es que crea múltiples copias de objetos al regresar, pasar a funciones, etc. Sin embargo, los compiladores inteligentes son conscientes de estas situaciones y se han optimizado bien para el rendimiento.
  • Es realmente tedioso en C ++ si la memoria se asigna y libera en dos lugares diferentes. La responsabilidad del lanzamiento es siempre una pregunta y, en su mayoría, confiamos en algunos punteros, objetos de pila (máximo posible) y técnicas comúnmente accesibles como auto_ptr (objetos RAII)
  • Lo mejor es que tiene control sobre la memoria y lo peor es que no tendrá ningún control sobre la memoria si empleamos una gestión de memoria inadecuada para la aplicación. Los bloqueos causados ​​por la corrupción de la memoria son los más desagradables y difíciles de rastrear.

55
En realidad, cualquier lenguaje que asigne memoria tiene un administrador de memoria, incluido c. La mayoría son muy simples, es decir, int * x = malloc (4); int * y = malloc (4); ... la primera llamada asignará memoria, es decir, solicitará la memoria del sistema operativo (generalmente en fragmentos de 1k / 4k) para que la segunda llamada, en realidad, no asigne memoria, sino que le proporcione una parte del último fragmento para el que se asignó. En mi opinión, los recolectores de basura no son administradores de memoria, ya que solo maneja la desasignación automática de la memoria. Para ser llamado administrador de memoria, no solo debe manejar la desasignación sino también la asignación de memoria.
Rahly

1
Las variables locales usan stack para que el compilador no emita llamadas malloc()o sus amigos para asignar la memoria requerida. Sin embargo, la pila no puede liberar ningún elemento dentro de la pila, la única forma en que se libera la memoria de la pila es desenrollando desde la parte superior de la pila.
Mikko Rantalainen

C ++ no "utiliza rutinas del sistema operativo"; eso no es parte del lenguaje, es solo una implementación común. C ++ incluso puede ejecutarse sin ningún sistema operativo.
einpoklum

22

Veo que se pierden algunas razones importantes para hacer la menor cantidad posible de novedades:

El operador newtiene un tiempo de ejecución no determinista

Llamar newpuede o no hacer que el sistema operativo asigne una nueva página física a su proceso, esto puede ser bastante lento si lo hace con frecuencia. O puede que ya tenga una ubicación de memoria adecuada lista, no lo sabemos. Si su programa necesita tener un tiempo de ejecución constante y predecible (como en un sistema en tiempo real o simulación de juego / física), debe evitar newen sus bucles críticos de tiempo.

El operador newes una sincronización de hilos implícita

Sí, me escuchó, su sistema operativo debe asegurarse de que sus tablas de páginas sean consistentes y, como tal, la llamada newhará que su hilo adquiera un bloqueo de exclusión mutua implícita. Si está llamando constantemente newdesde muchos subprocesos, en realidad está serializando sus subprocesos (lo he hecho con 32 CPU, cada uno de los cuales newobtiene unos cientos de bytes cada uno, ¡ay! Esa fue una pita real para depurar)

El resto, como lento, fragmentación, propensión a errores, etc., ya ha sido mencionado por otras respuestas.


2
Ambos pueden evitarse mediante la colocación de nuevo / eliminar y asignando la memoria de antemano. O puede asignar / liberar la memoria usted mismo y luego llamar al constructor / destructor. Esta es la forma en que std :: vector generalmente funciona.
rxantos

1
@rxantos Lea OP, esta pregunta trata sobre evitar asignaciones innecesarias de memoria. Además, no hay eliminación de ubicación.
Emily L.

@Emily Este es el significado de la OP, pienso:void * someAddress = ...; delete (T*)someAddress
xryl669

1
Usar stack tampoco es determinista en tiempo de ejecución. A menos que hayas llamado mlock()o algo similar. Esto se debe a que el sistema puede estar quedando sin memoria y no hay páginas de memoria física disponibles disponibles para la pila, por lo que el sistema operativo puede necesitar intercambiar o escribir algunas memorias caché (borrar la memoria sucia) en el disco antes de que la ejecución pueda continuar.
Mikko Rantalainen

1
@mikkorantalainen eso es técnicamente cierto, pero en una situación de poca memoria, todas las apuestas están apagadas de todos modos con el rendimiento del wrt mientras empuja al disco, por lo que no hay nada que pueda hacer. De ninguna manera invalida el consejo de evitar nuevas llamadas cuando sea razonable hacerlo.
Emily L.

21

Pre-C ++ 17:

Debido a que es propenso a fugas sutiles incluso si ajusta el resultado en un puntero inteligente .

Considere un usuario "cuidadoso" que recuerda envolver objetos en punteros inteligentes:

foo(shared_ptr<T1>(new T1()), shared_ptr<T2>(new T2()));

Este código es peligroso porque no hay garantía de que shared_ptrse construya antes que T1o T2. Por lo tanto, si uno new T1()o new T2()falla después del otro tiene éxito, entonces el primer objeto se filtrará porque no shared_ptrexiste para destruirlo y desasignarlo.

Solución: uso make_shared.

Post-C ++ 17:

Esto ya no es un problema: C ++ 17 impone una restricción en el orden de estas operaciones, en este caso asegurando que cada llamada a new()debe ser seguida inmediatamente por la construcción del puntero inteligente correspondiente, sin ninguna otra operación intermedia. Esto implica que, para cuando new()se llama al segundo , se garantiza que el primer objeto ya ha sido envuelto en su puntero inteligente, evitando así cualquier fuga en caso de que se produzca una excepción.

Barry proporcionó una explicación más detallada del nuevo orden de evaluación introducido por C ++ 17 en otra respuesta .

Gracias a @Remy Lebeau por señalar que esto sigue siendo un problema en C ++ 17 (aunque menos): el shared_ptrconstructor puede fallar al asignar su bloque de control y lanzar, en cuyo caso el puntero que se le pasó no se elimina.

Solución: uso make_shared.


55
Otra solución: nunca asigne dinámicamente más de un objeto por línea.
Antimonio

3
@Antimony: Sí, es mucho más tentador asignar más de un objeto cuando ya ha asignado uno, en comparación con cuando no ha asignado ninguno.
user541686

1
Creo que una mejor respuesta es que smart_ptr se filtrará si se llama a una excepción y nada la detecta.
Natalie Adams

2
Incluso en el caso posterior a C ++ 17, aún puede ocurrir una fuga si newtiene éxito y luego la shared_ptrconstrucción posterior falla. std::make_shared()eso también lo resolvería
Remy Lebeau

1
@Mehrdad, el shared_ptrconstructor en cuestión asigna memoria para un bloque de control que almacena el puntero y el borrador compartidos, por lo que sí, teóricamente puede arrojar un error de memoria. Solo los constructores copy, move y aliasing no se lanzan. make_sharedasigna el objeto compartido dentro del bloque de control, por lo que solo hay 1 asignación en lugar de 2.
Remy Lebeau

17

En gran medida, es alguien que eleva sus propias debilidades a una regla general. No hay nada malo per se con crear objetos usando el newoperador. Hay un argumento por el que debes hacerlo con cierta disciplina: si creas un objeto, debes asegurarte de que se destruirá.

La forma más fácil de hacerlo es crear el objeto en el almacenamiento automático, para que C ++ sepa destruirlo cuando salga del alcance:

 {
    File foo = File("foo.dat");

    // do things

 }

Ahora, observe que cuando se cae de ese bloque después del corsé final, fooestá fuera de alcance. C ++ llamará a su dtor automáticamente por usted. A diferencia de Java, no necesita esperar a que el GC lo encuentre.

Si hubieras escrito

 {
     File * foo = new File("foo.dat");

querrías emparejarlo explícitamente con

     delete foo;
  }

o incluso mejor, asigne su File *como un "puntero inteligente". Si no tiene cuidado con eso, puede provocar fugas.

La respuesta en sí hace la suposición errónea de que si no usa newno asigna en el montón; de hecho, en C ++ no lo sabes. A lo sumo, usted sabe que una pequeña cantidad de memoria, digamos un puntero, ciertamente está asignada en la pila. Sin embargo, considere si la implementación de File es algo así como

  class File {
    private:
      FileImpl * fd;
    public:
      File(String fn){ fd = new FileImpl(fn);}

a continuación, FileImplse sigue ser asignados en la pila.

Y sí, será mejor que te asegures de tener

     ~File(){ delete fd ; }

en la clase también; sin él, perderá memoria del montón incluso si aparentemente no asignó nada en el montón.


44
Debería echar un vistazo al código en la pregunta referenciada. Definitivamente hay muchas cosas que van mal en ese código.
André Caron

77
Estoy de acuerdo en que no hay nada de malo en usar new per se , pero si miras el código original al que hace referencia el comentario, newse está abusando de él. El código está escrito como si fuera Java o C #, donde newse usa para prácticamente todas las variables, cuando las cosas tienen mucho más sentido estar en la pila.
luke

55
Punto justo. Pero las reglas generales se hacen cumplir normalmente para evitar dificultades comunes. Ya sea que se trate de una debilidad individual o no, la administración de la memoria es lo suficientemente compleja como para garantizar una regla general como esta. :)
Robben_Ford_Fan_boy

99
@ Charlie: el comentario no dice que nunca deberías usar new. Dice que si tiene la opción entre asignación dinámica y almacenamiento automático, use el almacenamiento automático.
André Caron

8
@ Charlie: no hay nada de malo en usar new, pero si lo usas delete, ¡lo estás haciendo mal!
Matthieu M.

16

new()no debe usarse lo menos posible. Debe usarse con el mayor cuidado posible. Y debe usarse tan a menudo como sea necesario según lo dicte el pragmatismo.

La asignación de objetos en la pila, basándose en su destrucción implícita, es un modelo simple. Si el alcance requerido de un objeto se ajusta a ese modelo, entonces no hay necesidad de usarlo new(), con el asociado delete()y la comprobación de punteros NULL. En el caso de que tenga muchos objetos de corta duración, la asignación en la pila debería reducir los problemas de fragmentación del montón.

Sin embargo, si la vida útil de su objeto debe extenderse más allá del alcance actual, entonces new()es la respuesta correcta. Solo asegúrate de prestar atención a cuándo y cómo llamas delete()y a las posibilidades de los punteros NULL, usando objetos eliminados y todas las otras trampas que vienen con el uso de punteros.


99
"si la vida útil de su objeto debe extenderse más allá del alcance actual, entonces new () es la respuesta correcta" ... ¿por qué no devolver preferentemente por valor o aceptar una variable con alcance de llamador por no constreferencia o puntero ...?
Tony Delroy

2
@ Tony: ¡Sí, sí! Me alegra escuchar a alguien abogar por referencias. Fueron creados para prevenir este problema.
Nathan Osman

@TonyD ... o combínalos: devuelve un puntero inteligente por valor. De esta manera la persona que llama y en muchos casos (es decir, donde make_shared/_uniquese puede utilizar) el destinatario de la llamada no necesitan newo delete. Esta respuesta pierde los puntos reales: (A) C ++ proporciona cosas como RVO, semántica de movimiento y parámetros de salida, lo que a menudo significa que manejar la creación de objetos y la extensión de la vida al devolver memoria asignada dinámicamente se vuelve innecesario y descuidado. (B) Incluso en situaciones donde se requiere una asignación dinámica, stdlib proporciona envoltorios RAII que alivian al usuario de los detalles internos feos.
underscore_d

14

Cuando usa new, los objetos se asignan al montón. Generalmente se usa cuando anticipa una expansión. Cuando declaras un objeto como,

Class var;

Se coloca en la pila.

Siempre tendrá que llamar a destruir en el objeto que colocó en el montón con nuevo. Esto abre el potencial de pérdidas de memoria. ¡Los objetos colocados en la pila no son propensos a la pérdida de memoria!


2
+1 "[montón] generalmente se usa cuando anticipa una expansión", como agregar a una std::stringo std::map, sí, una visión aguda. Mi reacción inicial fue "pero también muy comúnmente para desacoplar la vida útil de un objeto del alcance del código de creación", pero realmente es mejor regresar por valor o aceptar valores con alcance de llamador por no constreferencia o puntero, excepto cuando hay una "expansión" involucrada también. Sin embargo, hay otros usos del sonido como los métodos de fábrica ...
Tony Delroy

12

Una razón notable para evitar el uso excesivo del montón es el rendimiento, que involucra específicamente el rendimiento del mecanismo de administración de memoria predeterminado utilizado por C ++. Si bien la asignación puede ser bastante rápida en el caso trivial, hacer mucho newy deleteen objetos de tamaño no uniforme sin un orden estricto conduce no solo a la fragmentación de la memoria, sino que también complica el algoritmo de asignación y puede destruir absolutamente el rendimiento en ciertos casos.

Ese es el problema que los grupos de memoria se crearon para resolver, lo que permite mitigar las desventajas inherentes de las implementaciones de almacenamiento dinámico tradicionales, al tiempo que le permite utilizar el almacenamiento dinámico según sea necesario.

Sin embargo, es mejor evitar el problema por completo. Si puedes ponerlo en la pila, entonces hazlo.


Siempre puede asignar una cantidad de memoria razonablemente grande y luego usar la ubicación nueva / eliminar si la velocidad es un problema.
rxantos

Los grupos de memoria deben evitar la fragmentación, acelerar la desasignación (una desasignación para miles de objetos) y hacer que la desasignación sea más segura.
Lothar

10

Creo que el cartel quería decir You do not have to allocate everything on theheapmás que el stack.

Básicamente, los objetos se asignan en la pila (si el tamaño del objeto lo permite, por supuesto) debido al bajo costo de la asignación de la pila, en lugar de la asignación basada en el montón que implica bastante trabajo por parte del asignador, y agrega verbosidad porque entonces tiene que administrar datos asignados en el montón.


10

Tiendo a estar en desacuerdo con la idea de usar nuevos "demasiado". Aunque el uso del póster original de nuevo con clases de sistema es un poco ridículo. ( int *i; i = new int[9999];? realmente? int i[9999];es mucho más claro). Creo que eso es lo que estaba obteniendo la cabra del comentarista.

Cuando trabaja con objetos del sistema, es muy raro que necesite más de una referencia al mismo objeto. Mientras el valor sea el mismo, eso es todo lo que importa. Y los objetos del sistema no suelen ocupar mucho espacio en la memoria. (un byte por carácter, en una cadena). Y si lo hacen, las bibliotecas deberían estar diseñadas para tener en cuenta esa administración de memoria (si están bien escritas). En estos casos, (todas menos una o dos de las noticias en su código), nuevo es prácticamente inútil y solo sirve para introducir confusiones y posibles errores.

Sin embargo, cuando trabaje con sus propias clases / objetos (por ejemplo, la clase de línea del póster original), debe comenzar a pensar en cuestiones como la huella de la memoria, la persistencia de los datos, etc. En este punto, permitir múltiples referencias al mismo valor es invaluable: permite construcciones como listas vinculadas, diccionarios y gráficos, donde las variables múltiples no solo deben tener el mismo valor, sino también hacer referencia al mismo objeto en la memoria. Sin embargo, la clase Line no tiene ninguno de esos requisitos. Por lo tanto, el código del póster original no tiene absolutamente ninguna necesidad new.


por lo general, el nuevo / eliminar se usaría cuando no conozca de antemano el tamaño de la matriz. Por supuesto, std :: vector oculta nuevo / eliminar para usted. Todavía los usas, pero a través de std :: vector. Por lo tanto, hoy en día se usaría cuando no conozca el tamaño de la matriz y desee, por alguna razón, evitar la sobrecarga de std :: vector (que es pequeño, pero aún existe).
rxantos

When you're working with your own classes/objects... a menudo no tienes razón para hacerlo! Una pequeña proporción de Qs están en detalles del diseño de contenedores por codificadores expertos. En marcado contraste, una proporción deprimente tiene que ver con la confusión de los novatos que no saben que existe el stdlib, o se les asignan tareas terribles en cursos de 'programación', donde un tutor exige que reinventen la rueda sin sentido, incluso antes de que incluso aprendí qué es una rueda y por qué funciona. Al promover una asignación más abstracta, C ++ puede salvarnos del interminable 'segfault con la lista vinculada'; por favor, vamos a dejarlo .
underscore_d

" El uso del cartel original de nuevo con clases de sistema es un poco ridículo. ( int *i; i = new int[9999];? Realmente? int i[9999];es mucho más claro)." Sí, es más claro, pero para jugar al abogado del diablo, el tipo no es necesariamente un mal argumento. Con 9999 elementos, puedo imaginar un sistema embebido ajustado que no tiene suficiente pila para 9999 elementos: 9999x4 bytes es ~ 40 kB, x8 ~ 80 kB. Por lo tanto, estos sistemas pueden necesitar la asignación dinámica, suponiendo que la implementen usando memoria alternativa. Aún así, eso solo podría justificar la asignación dinámica, no new; a vectorsería la solución real en ese caso
underscore_d

De acuerdo con @underscore_d, ese no es un gran ejemplo. No agregaría 40,000 u 80,000 bytes a mi pila así como así. En realidad, probablemente los asignaría en el montón (con, std::make_unique<int[]>()por supuesto).
einpoklum

3

Dos razones:

  1. Es innecesario en este caso. Estás haciendo tu código innecesariamente más complicado.
  2. Asigna espacio en el montón y significa que debe recordarlo deletemás tarde, o provocará una pérdida de memoria.

2

newEs lo nuevo goto.

Recuerde por qué gotoestá tan vilipendiado: si bien es una herramienta poderosa de bajo nivel para el control de flujo, las personas a menudo la usaron de maneras innecesariamente complicadas que dificultaban el seguimiento del código. Además, los patrones más útiles y fáciles de leer se codificaron en declaraciones de programación estructuradas (por ejemplo, foro while); El efecto final es que el código donde gotoestá la forma apropiada es bastante raro, si estás tentado a escribir goto, probablemente estés haciendo las cosas mal (a menos que realmente sepas lo que estás haciendo).

newes similar: a menudo se usa para hacer que las cosas sean innecesariamente complicadas y más difíciles de leer, y los patrones de uso más útiles que se pueden codificar se han codificado en varias clases. Además, si necesita utilizar nuevos patrones de uso para los que aún no hay clases estándar, ¡puede escribir sus propias clases que las codifiquen!

Incluso diría que newes peor que goto, debido a la necesidad de emparejar newy hacer deletedeclaraciones.

Por ejemplo goto, si alguna vez cree que necesita usar new, probablemente esté haciendo las cosas mal, especialmente si lo hace fuera de la implementación de una clase cuyo propósito en la vida es encapsular las asignaciones dinámicas que necesita hacer.


Y agregaría: "Básicamente no lo necesitas".
einpoklum

1

La razón principal es que los objetos en el montón son siempre difíciles de usar y administrar que los valores simples. Escribir código que sea fácil de leer y mantener es siempre la primera prioridad de cualquier programador serio.

Otro escenario es que la biblioteca que estamos utilizando proporciona semántica de valor y hace innecesaria la asignación dinámica. Std::stringEs un buen ejemplo.

Sin embargo, para el código orientado a objetos new, es obligatorio usar un puntero, lo que significa usarlo para crearlo de antemano. Para simplificar la complejidad de la gestión de recursos, tenemos docenas de herramientas para que sea lo más simple posible, como los punteros inteligentes. El paradigma basado en objetos o el paradigma genérico asume una semántica de valor y requiere menos o no new, tal como lo indican los carteles en otras partes.

Los patrones de diseño tradicionales, especialmente los mencionados en el libro GoF , se usan newmucho, ya que son códigos OO típicos.


44
Esta es una respuesta abismal . For object oriented code, using a pointer [...] is a must: sin sentido . Si está devaluando 'OO' al referirse solo a un pequeño subconjunto, polimorfismo , también sin sentido: las referencias también funcionan. [pointer] means use new to create it beforehand: especialmente sin sentido : se pueden tomar referencias o punteros a objetos asignados automáticamente y utilizarlos polimórficamente; vigilarme . [typical OO code] use new a lot: tal vez en algún libro viejo, pero a quién le importa? Cualquier vagabundamente moderno C ++ evita new/ apuntadores crudos siempre que sea posible, y de ninguna manera es menos OO al hacerlo
underscore_d

1

Un punto más a todas las respuestas correctas anteriores, depende de qué tipo de programación esté haciendo. Kernel que se desarrolla en Windows, por ejemplo -> La pila está severamente limitada y es posible que no pueda tomar fallas de página como en el modo de usuario.

En tales entornos, se prefieren e incluso se requieren llamadas API nuevas o similares a C.

Por supuesto, esto es simplemente una excepción a la regla.


Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.