sintaxis - ¿Por qué deberían los programadores de C++ minimizar el uso de ''nuevo''?
sintaxis de c++ (17)
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
new
tanto. No puedo ver ninguna razón por la que usaste algo nuevo donde 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 lo que quiere decir con eso. ¿Por qué los objetos deben crearse por valor en C ++ con la mayor frecuencia posible, y qué diferencia hace internamente? ¿Malinterpreté la respuesta?
Pre-C ++ 17:
Porque es propenso a fugas sutiles incluso si envuelve 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_ptr
se construya antes que T1
o T2
. Por lo tanto, si uno de los new T1()
o new T2()
falla después de que el otro tenga éxito, entonces el primer objeto se shared_ptr
porque no existe shared_ptr
para destruirlo y desasignarlo.
Solución: utilizar 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 en Entre. Esto implica que, en el momento en que se llame al segundo new()
, 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 .
Porque la pila es rápida e infalible.
En C ++, solo se necesita una única instrucción para asignar espacio (en la pila) para cada objeto de alcance local en una función determinada, y es imposible perder nada de esa memoria. Ese comentario pretendía (o debería haber intentado) decir algo como "usa la pila y no el montón".
Creo que el cartel quería decir You do not have to allocate everything on the
heap
lugar de hacerlo en la 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 los datos asignados en el montón.
Cuando se usa nuevo, los objetos se asignan al montón. Generalmente se usa cuando se anticipa expansión. Cuando declara un objeto como, por ejemplo,
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 para fugas de memoria. ¡Los objetos colocados en la pila no son propensos a fugas de memoria!
Dos razones:
- Es innecesario en este caso. Estás haciendo tu código innecesariamente más complicado.
- Asigna espacio en el montón, y significa que debe acordarse de
delete
más tarde o provocará una pérdida de memoria.
En gran medida, es alguien que eleva sus propias debilidades a una regla general. No hay nada malo en sí mismo con la creación de objetos con el new
operador. Para lo que hay algún argumento es que debes hacerlo con cierta disciplina: si creas un objeto, necesitas asegurarte de que sea destruido.
La forma más sencilla de hacerlo es crear el objeto en el almacenamiento automático, por lo que C ++ sabe destruirlo cuando se sale del alcance:
{
File foo = File("foo.dat");
// do things
}
Ahora, observa que cuando te caes de ese bloque después de la abrazadera final, foo
está fuera de alcance. C ++ llamará a su dtor automáticamente por ti. A diferencia de Java, no es necesario esperar a que el GC lo encuentre.
Te habia escrito
{
File * foo = new File("foo.dat");
Usted querría hacerlo coincidir explícitamente con
delete foo;
}
o incluso mejor, asigne su File *
como un "puntero inteligente". Si no tienes cuidado con eso, puede provocar fugas.
La respuesta en sí misma hace la suposición errónea de que si no usa una new
no se 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 se asigna en la pila. Sin embargo, considera si la implementación de File es algo como
class File {
private:
FileImpl * fd;
public:
File(String fn){ fd = new FileImpl(fn);}
entonces FileImpl
todavía se asignará en la pila.
Y sí, mejor asegúrate de tener
~File(){ delete fd ; }
en la clase también; sin ella, perderá memoria del montón incluso si aparentemente no lo asignó en absoluto.
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é pasa si "Hacer cosas" lanza una excepción? Ups: pérdida de memoria. ¿Qué sucede si los problemas de "Hacer cosas" return
antes? Ups: pérdida de memoria.
Y esto es para el caso más simple . Si le devuelves esa cadena a alguien, ahora tienen que borrarla. Y si lo pasan como un argumento, ¿la persona que lo recibe necesita borrarlo? ¿Cuándo deberían eliminarlo?
O, simplemente puede hacer esto:
std::string someString(...);
//Do stuff
No delete
. El objeto fue creado en la "pila", y será destruido una vez que esté fuera del alcance. Incluso puede devolver el objeto, transfiriendo así su contenido a la función de llamada. Puede pasar el objeto a las funciones (generalmente como referencia o const-reference: void SomeFunc(std::string &iCanModifyThis, const std::string &iCantModifyThis)
. Y así sucesivamente.
Todo sin new
y delete
. No hay duda de quién es el dueño de la memoria o quién es responsable de borrarlo. Si lo haces:
std::string someString(...);
std::string otherString;
otherString = someString;
Se entiende que otherString
tiene una copia de los datos de someString
. No es un puntero; Es un objeto separado. Puede suceder que tengan los mismos contenidos, pero puede cambiar uno sin afectar al otro:
someString += "More text.";
if(otherString == someString) { /*Will never get here */ }
¿Ves la idea?
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 correspondiente de memoria para cada uno: la pila y el montón.
Apilar
La pila siempre asigna memoria de una manera 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 es implícita.
En C ++, esto se denomina 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 mediante {}
), se recopila automáticamente la memoria de todas las variables en ese bloque. Este es también el momento en el que se invoca a 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, utilizando 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 utilizar 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 adecuados para la asignación dinámica, ya que es menos limitado.
Dos razones clave para usar la asignación dinámica:
No sabes cuánta memoria necesitas en tiempo de compilación. Por ejemplo, al leer 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 dejar el bloque actual. Por ejemplo, es posible que desee escribir un
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 conservar el bloque de memoria asignado.
¿Por qué la asignación dinámica es a menudo innecesaria?
En C ++ hay una construcción ordenada llamada destructor . Este mecanismo le permite administrar recursos al alinear 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 ++. Se "envuelve" los recursos en objetos. std::string
es un ejemplo perfecto. Este fragmento de código:
int main ( int argc, char* argv[] )
{
std::string program(argv[0]);
}
en realidad asigna una cantidad variable de memoria. El objeto std::string
asigna memoria utilizando el montón y lo libera en su destructor. En este caso, no fue necesario administrar manualmente ningún recurso y aún así obtuvo los beneficios de la asignación de memoria dinámica.
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 una asignación de memoria dinámica innecesaria. El programa requiere más escritura (!) E introduce el riesgo de olvidarse de desasignar la memoria. Lo hace sin beneficio aparente.
¿Por qué debería utilizar el almacenamiento automático tan a menudo como sea posible?
Básicamente, el último párrafo lo resume. El uso de almacenamiento automático con la mayor frecuencia posible hace que sus programas:
- más rápido de escribir;
- más rápido cuando se ejecuta;
- Menos propensos a la memoria / fugas de recursos.
Puntos extra
En la pregunta referida, 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;
}
Usando la versión original, es probable que este programa se bloquee, ya que utiliza delete
en la misma cadena dos veces. Usando la versión modificada, cada instancia de Line
tendrá su propia instancia de cadena, cada una con su propia memoria y ambas se lanzarán 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 se compone . Se escala.
Si usas la clase Line
como un bloque de construcción:
class Table
{
Line borders[4];
};
Entonces
int main ()
{
Table table;
}
asigna cuatro instancias std::string
, cuatro instancias de Line
, una instancia de Table
y todo el contenido de la cadena y todo se libera automáticamente .
La razón principal es que los objetos en el montón siempre son difíciles de usar y administrar que los valores simples. Escribir un 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 usando proporciona semántica de valor y hace que la asignación dinámica sea innecesaria. Std::string
es un buen ejemplo.
Sin embargo, para el código orientado a objetos, usar un puntero (lo que significa usar un new
para crearlo de antemano) es una necesidad.Para simplificar la complejidad de la administración de recursos, tenemos docenas de herramientas para hacerlo lo más simple posible, como los punteros inteligentes. El paradigma basado en objetos o el paradigma genérico asume un valor semántico y requiere menos o no new
, tal como lo indican los carteles en otros lugares.
Los patrones de diseño tradicionales, especialmente los mencionados en el libro de GoF , usan new
mucho, ya que son códigos OO típicos.
Los objetos creados por new
deben delete
eventualmente para que no se filtren. El destructor no será llamado, la memoria no será liberada, 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 como auto_ptr
, shared_ptr
resuelven el problema de referencia, pero requieren una disciplina de codificación y tienen otros problemas (copiabilidad, bucles de referencia, etc.).
Además, en escenarios con múltiples subprocesos, lo new
es un punto de contención entre los hilos; Puede haber un impacto en el rendimiento por el uso excesivo de new
. La creación de objetos de pila es, por definición, subproceso local, ya que cada subproceso tiene su propia pila.
La desventaja de los objetos de valor es que mueren una vez que se devuelve la función de host: no puede pasar una referencia a los que devuelven la llamada, solo copiando o devolviendo por valor.
Tiendo a estar en desacuerdo con la idea de usar el nuevo "demasiado". Aunque el uso del póster original de nuevas con clases de sistema es un poco ridículo. ( int *i; i = new int[9999];
really? 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 normalmente no ocupan 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 tomar en cuenta la administración de la memoria (si están bien escritas). En estos casos, (todas menos una o dos de las noticias en su código), las novedades son prácticamente inútiles y solo sirven para introducir confusiones y posibles errores.
Sin embargo, cuando trabaja con sus propias clases / objetos (p. Ej., La clase Line del póster original), debe comenzar a pensar en los problemas como la huella de memoria, la persistencia de los datos, etc. En este punto, permitir múltiples referencias al mismo valor es invaluable: permite construcciones como listas enlazadas, diccionarios y gráficas, donde varias variables 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 original del cartel no tiene absolutamente ninguna necesidad de new
.
Una razón importante para evitar el uso excesivo del montón es el rendimiento, que implica 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 muchas cosas new
y delete
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 crearon las agrupaciones de memoria para resolver, lo que permite mitigar las desventajas inherentes de las implementaciones de pila tradicionales, al tiempo que le permite usar la pila según sea necesario.
Mejor aún, sin embargo, para evitar el problema por completo. Si puedes ponerlo en la pila, hazlo.
Veo que se pierden algunas razones importantes para hacer lo menos posible:
Operador new
tiene un tiempo de ejecución no determinista.
Llamar 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 a menudo. O puede que ya tenga lista una ubicación de memoria adecuada, no lo sabemos. Si su programa necesita tener un tiempo de ejecución consistente y predecible (como en un sistema en tiempo real o en un juego / simulación física), debe evitar new
bucles en sus tiempos críticos.
Operador new
es una sincronización de hilo implícita
Sí, me ha escuchado, su sistema operativo debe asegurarse de que las tablas de sus páginas sean coherentes y, por lo tanto, las new
llamadas harán que su hilo adquiera un bloqueo mutex implícito. Si constantemente estás llamando a new
desde muchos subprocesos, en realidad estás serializando tus subprocesos (lo he hecho con 32 CPU, cada uno de los cuales es new
para obtener unos pocos cientos de bytes cada uno, ¡ay! Esa fue una pita real para depurar)
El resto, como lento, fragmentación, propenso a errores, etc. ya han sido mencionados por otras respuestas.
new()
no debe usarse lo menos posible. Debe ser utilizado lo más cuidadosamente posible. Y debe usarse tan a menudo como sea necesario según lo dictado por 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 es necesario usar new()
, con el comando delete()
asociado y la comprobación de los punteros NULL. En el caso de que tenga una gran cantidad de 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 necesita extenderse más allá del alcance actual, entonces new()
es la respuesta correcta. Solo asegúrese de prestar atención a cuándo y cómo llama a delete()
y las posibilidades de los punteros NULL, utilizando objetos eliminados y todos los otros errores que vienen con el uso de punteros.
new
asigna objetos en el montón. De lo contrario, los objetos se asignan en la pila. Busca la diferencia entre los dos .
new
es el nuevo goto
.
Recuerde por qué goto
está tan vilipendiado: si bien es una herramienta poderosa y de bajo nivel para el control de flujo, la gente a menudo la usó en formas innecesariamente complicadas que hicieron que el código fuera difícil de seguir. Además, los patrones más útiles y más fáciles de leer fueron codificados en declaraciones de programación estructurada (por ejemplo, for
o while
); el efecto final es que el código donde goto
es apropiado es bastante raro, si está tentado a escribir goto
, probablemente esté haciendo las cosas mal (a menos que sepa realmente lo que está haciendo).
new
es similar: a menudo se utiliza para hacer las cosas innecesariamente complicadas y 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 no haya clases estándar, ¡puede escribir sus propias clases que las codifiquen!
Incluso diría que new
es peor que goto
, debido a la necesidad de emparejar new
y las delete
declaraciones.
Por ejemplo goto
, si alguna vez piensa que necesita usarlo 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 resumir las asignaciones dinámicas que necesita hacer.
- C ++ no emplea ningún administrador de memoria por sí mismo. Otros lenguajes como C #, Java tiene recolector de basura para manejar la memoria
- C ++ utiliza 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 utiliza con frecuencia, es aconsejable asignarla previamente y liberarla cuando no sea necesaria.
- La gestión incorrecta de la memoria puede provocar pérdidas de memoria y es realmente difícil de rastrear. Así que 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 han sido optimizados para el rendimiento.
- Es realmente tedioso en C ++ si la memoria se asigna y se libera en dos lugares diferentes. La responsabilidad de la publicación es siempre una pregunta y, en su mayoría, dependemos de algunos punteros, objetos de pila (máximo posible) y técnicas como auto_ptr (objetos RAII).
- Lo mejor es que tienes control sobre la memoria y lo peor es que no tendrás ningún control sobre la memoria si empleamos una administración de memoria incorrecta para la aplicación. Los bloqueos causados por daños en la memoria son los más desagradables y difíciles de rastrear.