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 delete
o delete[]
( free
en 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::string
Es 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::string
objeto 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::string
define 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 delete
en la misma cadena dos veces. Usando la versión modificada, cada Line
instancia 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 Line
clase como un bloque de construcción:
class Table
{
Line borders[4];
};
Entonces
int main ()
{
Table table;
}
asigna cuatro std::string
instancias, cuatro Line
instancias, una Table
instancia y todos los contenidos de la cadena y todo se libera automáticamente .
Monster
escupeTreasure
aWorld
cuando muere. En suDie()
método, agrega el tesoro al mundo. Debe usarseworld->Add(new Treasure(/*...*/))
en otro para preservar el tesoro después de que muera. Las alternativas sonshared_ptr
(pueden ser excesivas),auto_ptr
(semántica deficiente para la transferencia de propiedad), pasar por valor (derrochador) ymove
+unique_ptr
(aún no se ha implementado ampliamente).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".
fuente
int x; return &x;
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:
Esto es simple. Pero, ¿qué sucede si "Hacer cosas" arroja una excepción? Vaya: pérdida de memoria. ¿Qué sucede si "Do stuff" emite
return
temprano? 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:
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
new
ydelete
. No se trata de quién posee la memoria o quién es responsable de eliminarla. Si lo haces:Se entiende que
otherString
tiene una copia de los datos desomeString
. No es un puntero; Es un objeto separado. Puede que tengan el mismo contenido, pero puede cambiar uno sin afectar al otro:¿Ves la idea?
fuente
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 esonew
.Los objetos creados por
new
deben ser eventualmentedelete
no 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_ptr
resuelven 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,
new
es un punto de contención entre hilos; Puede haber un impacto en el rendimiento por el uso excesivonew
. 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.
fuente
new
deben ser eventualmentedelete
no sea que se filtren". - peor aún,new[]
debe coincidir condelete[]
, y obtendrá un comportamiento indefinido sidelete
new[]
-edió la memoria odelete[]
new
-edió la memoria - muy pocos compiladores advierten sobre esto (algunas herramientas como Cppcheck lo hacen cuando pueden).fuente
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.Veo que se pierden algunas razones importantes para hacer la menor cantidad posible de novedades:
El operador
new
tiene un tiempo de ejecución no deterministaLlamar
new
puede 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 evitarnew
en sus bucles críticos de tiempo.El operador
new
es una sincronización de hilos implícitaSí, me escuchó, su sistema operativo debe asegurarse de que sus tablas de páginas sean consistentes y, como tal, la llamada
new
hará que su hilo adquiera un bloqueo de exclusión mutua implícita. Si está llamando constantementenew
desde muchos subprocesos, en realidad está serializando sus subprocesos (lo he hecho con 32 CPU, cada uno de los cualesnew
obtiene 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.
fuente
void * someAddress = ...; delete (T*)someAddress
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.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:
Este código es peligroso porque no hay garantía de que
shared_ptr
se construya antes queT1
oT2
. Por lo tanto, si unonew T1()
onew T2()
falla después del otro tiene éxito, entonces el primer objeto se filtrará porque noshared_ptr
existe 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 anew()
debe ser seguida inmediatamente por la construcción del puntero inteligente correspondiente, sin ninguna otra operación intermedia. Esto implica que, para cuandonew()
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_ptr
constructor 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
.fuente
new
tiene éxito y luego lashared_ptr
construcción posterior falla.std::make_shared()
eso también lo resolveríashared_ptr
constructor 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_shared
asigna el objeto compartido dentro del bloque de control, por lo que solo hay 1 asignación en lugar de 2.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
new
operador. 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:
Ahora, observe que cuando se cae de ese bloque después del corsé final,
foo
está 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
querrías emparejarlo explícitamente con
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
new
no 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í comoa continuación,
FileImpl
se sigue ser asignados en la pila.Y sí, será mejor que te asegures de tener
en la clase también; sin él, perderá memoria del montón incluso si aparentemente no asignó nada en el montón.
fuente
new
per se , pero si miras el código original al que hace referencia el comentario,new
se está abusando de él. El código está escrito como si fuera Java o C #, dondenew
se usa para prácticamente todas las variables, cuando las cosas tienen mucho más sentido estar en la pila.new
. Dice que si tiene la opción entre asignación dinámica y almacenamiento automático, use el almacenamiento automático.new
, pero si lo usasdelete
, ¡lo estás haciendo mal!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 asociadodelete()
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 llamasdelete()
y a las posibilidades de los punteros NULL, usando objetos eliminados y todas las otras trampas que vienen con el uso de punteros.fuente
const
referencia o puntero ...?make_shared/_unique
se puede utilizar) el destinatario de la llamada no necesitannew
odelete
. 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.Cuando usa new, los objetos se asignan al montón. Generalmente se usa cuando anticipa una expansión. Cuando declaras un objeto como,
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!
fuente
std::string
ostd::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 noconst
referencia 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 ...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
new
ydelete
en 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.
fuente
Creo que el cartel quería decir
You do not have to allocate everything on the
heap
más que elstack
.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.
fuente
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
.fuente
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 .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, nonew
; avector
sería la solución real en ese casostd::make_unique<int[]>()
por supuesto).Dos razones:
delete
más tarde, o provocará una pérdida de memoria.fuente
new
Es lo nuevogoto
.Recuerde por qué
goto
está 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,for
owhile
); El efecto final es que el código dondegoto
está la forma apropiada es bastante raro, si estás tentado a escribirgoto
, probablemente estés haciendo las cosas mal (a menos que realmente sepas lo que estás haciendo).new
es 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
new
es peor quegoto
, debido a la necesidad de emparejarnew
y hacerdelete
declaraciones.Por ejemplo
goto
, si alguna vez cree que necesita usarnew
, 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.fuente
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::string
Es 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 nonew
, tal como lo indican los carteles en otras partes.Los patrones de diseño tradicionales, especialmente los mencionados en el libro GoF , se usan
new
mucho, ya que son códigos OO típicos.fuente
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 ++ evitanew
/ apuntadores crudos siempre que sea posible, y de ninguna manera es menos OO al hacerloUn 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.
fuente
new
asigna objetos en el montón. De lo contrario, los objetos se asignan en la pila. Busca la diferencia entre los dos .fuente