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


Me encontré con la pregunta de Stack Overflow Fuga de memoria con std :: cadena cuando se usa std :: list <std :: string>y uno de los comentarios dice esto:

Dejar de usar new tanto. No puedo ver ninguna razón por la que usaste algo nuevo en ningún lado   lo hiciste. Puede crear objetos por valor en C ++ y es uno de los   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é se deben crear objetos por valor en C ++ tan a menudo como sea posible, y qué diferencia hace internamente? ¿He malinterpretado la respuesta?


750
2018-06-28 00:08


origen


Respuestas:


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 forma secuencial. Puede hacerlo porque requiere que libere la memoria en el orden inverso (First-In, Last-Out: 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 llama almacenamiento automático porque el almacenamiento se reclama automáticamente al final del alcance. Tan pronto como la ejecución del bloque de código actual (delimitado usando {}) se completa, la memoria para todas las variables en ese bloque se recopila automáticamente. Este es también el momento donde destructores se invocan 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 lanzamiento implícito, debe liberar la memoria manualmente, utilizando delete o delete[] (free Cª). Sin embargo, la ausencia de un punto de lanzamiento implícito es la clave de la flexibilidad del montón.

Razones para usar la asignación dinámica

Incluso si el uso del almacenamiento dinámico es más lento y puede generar fugas 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 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 devuelve 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 incinerador de basuras. 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. los std::string objeto asigna memoria utilizando el montón y lo libera en su destructor. En este caso, lo hiciste no necesita administrar manualmente cualquier recurso y aún así obtener 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 tipeo (!) E introduce el riesgo de olvidarse de desasignar la memoria. Lo hace sin 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 de almacenamiento automático tan a menudo como sea posible hace que sus programas:

  • más rápido para escribir;
  • más rápido cuando se ejecuta;
  • menos propenso a fugas de memoria / recursos.

Puntos extra

En la pregunta a la que se hace referencia, 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.
}

El motivo es que std::string define apropiadamente 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 falle, ya que usa delete en la misma cuerda dos veces. Usando la versión modificada, cada Line instancia poseerá su propia cadena ejemplo, cada uno con su propia memoria y ambos serán lanzados al final del programa.

Otras notas

Amplio uso 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 usas el 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, uno Table instancia y todos los contenidos de la cadena y todo se libera automágicamente.


898
2018-06-28 00:47



Porque la pila es rápida y a prueba de tontos

En C ++, se necesita una sola instrucción para asignar espacio, en la pila, para cada objeto de ámbito local en una función dada, y es imposible filtrar cualquiera de esa memoria. Ese comentario pretendía (o debería haber tenido la intención) decir algo así como "usa la pila y no el montón".


155
2018-06-28 00:14



Es complicado.

Primero, C ++ no es basura recolectada. Por lo tanto, para cada nuevo, debe haber una eliminación correspondiente. Si no puedes poner esta eliminación, entonces tienes 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é ocurre si "Hacer cosas" arroja una excepción? Oops: pérdida de memoria. ¿Qué sucede si los problemas de "Hacer cosas" return ¿temprano? Oops: pérdida de memoria.

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

O bien, puedes hacer esto:

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

No delete. El objeto fue creado 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). Etcétera.

Todo sin new y delete. No hay duda de a quién pertenece la memoria o quién es el responsable de borrarla. Si lo haces:

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

Se entiende que otherString tiene una copia del datos de someString. No es un puntero; es un objeto separado Es posible que tengan los mismos contenidos, pero puede cambiar uno sin afectar al otro:

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

Ver la idea?


94
2018-06-28 00:17



Objetos creados por new debe ser eventualmente deleted ellos se escapan. No se llamará al destructor, la memoria no se liberará, 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. La llamada al destructor es insertada por el compilador, y la memoria se libera automáticamente al regresar la función.

Punteros inteligentes como auto_ptr, shared_ptr resolver el problema de referencia colgante, pero requieren disciplina de codificación y tienen otros problemas (capacidad de copiado, bucles de referencia, etc.).

Además, en escenarios altamente multiproceso, new es un punto de disputa entre hilos; puede haber un impacto en el rendimiento por uso excesivo new. La creación de objetos de pila es, por definición, local de subprocesos, ya que cada subproceso tiene su propia pila.

La desventaja de los objetos de valor es que mueren una vez que regresa la función de host; no se puede pasar una referencia a los que regresan a la persona que llama, solo copiando o devolviendo por valor.


65
2018-06-28 00:11



  • C ++ no emplea ningún administrador de memoria por sí mismo. Otros lenguajes como C #, Java tiene un recolector de basura para manejar la memoria
  • C ++ utilizando las rutinas del sistema operativo para asignar la memoria y demasiado nuevo / eliminar podría fragmentar la memoria disponible
  • Con cualquier aplicación, si la memoria se usa con frecuencia, es aconsejable asignarla previamente y liberarla cuando no sea necesario.
  • La administración incorrecta de la memoria puede provocar pérdidas de memoria y es muy difícil realizar un seguimiento. Entonces, usar objetos de pila dentro del alcance de la función es una técnica probada
  • La desventaja del uso de objetos de pila es que crea copias múltiples de objetos al regresar, pasando a funciones, etc. Sin embargo, los compiladores inteligentes son muy conscientes de estas situaciones y se han optimizado bien para el rendimiento.
  • Es realmente tedioso en C ++ si la memoria se asigna y se libera en dos lugares diferentes. La responsabilidad del lanzamiento es siempre una cuestión y, en su mayoría, confiamos en algunos punteros comúnmente accesibles, objetos apilados (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 gestión de memoria incorrecta para la aplicación. Los bloqueos causados ​​por la corrupción de la memoria son los más desagradables y difíciles de rastrear.

27
2018-06-28 02:59



En gran medida, eso es alguien elevando sus propias debilidades a una regla general. No hay nada malo per se con crear objetos usando el new operador. Lo que hay que argumentar es 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, 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 del corsé final, foo está fuera del alcance. C ++ llamará automáticamente a su dtor. A diferencia de Java, no necesita esperar a que el GC lo encuentre.

Si hubieras escrito

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

querrías unirlo explícitamente con

     delete foo;
  }

o mejor aún, asigna tu File * como un "puntero inteligente". Si no tiene cuidado acerca de eso, puede provocar fugas.

La respuesta en sí misma supone erróneamente que, si no se usa new no se asigna en el montón; de hecho, en C ++ no lo sabes. A lo sumo, sabrá que una pequeña cantidad de memoria, por ejemplo, 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);}

entonces FileImpl será todavía ser asignado en la pila.

Y sí, es mejor que te asegures de tener

     ~File(){ delete fd ; }

en la clase también; sin él, perderás memoria del montón, incluso si no lo hiciste aparentemente asignar en el montón a todos.


17
2018-06-28 00:11



Veo que faltan algunas razones importantes para hacer tan pocas novedades como sea posible:

Operador new tiene un tiempo de ejecución no determinista

Vocación 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 una ubicación de memoria adecuada lista, no lo sabemos. Si su programa necesita tener un tiempo de ejecución consistente y predecible (como en un sistema en tiempo real o simulación de juego / física), debe evitar new en tu tiempo, loops críticos.

Operador new es una sincronización de hilos implícita

Sí, me oíste, tu sistema operativo debe asegurarse de que las tablas de tus páginas sean coherentes y, como tal, new hará que su hilo adquiera un bloqueo mutex implícito. Si llamas constantemente new de muchos hilos estás serializando tus hilos (he hecho esto con 32 CPUs, cada uno golpeando new para obtener unos cientos de bytes cada uno, ¡ay! esa era una p.i.t.a. depurar)

El resto, como la lentitud, la fragmentación, la propensión a errores, etc. ya han sido mencionados por otras respuestas.


16
2018-02-12 17:57



Cuando utiliza nuevo, los objetos se asignan al montón. Generalmente se usa cuando anticipas la expansión. Cuando declara un objeto como,

Class var;

se coloca en la pila.

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


13
2018-06-28 00:10



new() no debe usarse como pequeño como sea posible. Se debe usar como cuidadosamente como sea posible. Y debe usarse tantas veces como sea necesario según lo dictado por el pragmatismo.

La asignación de objetos en la pila, confiando 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 usar new(), con el asociado delete() y comprobación de punteros NULL. En el caso de que tenga muchos objetos de vida corta, la asignación en la pila debería reducir los problemas de fragmentación del montón.

Sin embargo, si la duración de su objeto necesita 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 las posibilidades de los punteros NULL, utilizando objetos eliminados y todos los otros errores que vienen con el uso de punteros.


13
2018-06-28 00:38



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 hay sin garantía Eso tampoco shared_ptr esta construido antes de ya sea T1 o T2. Por lo tanto, si uno de new T1() o new T2() falla después de que el otro tiene éxito, entonces el primer objeto se filtró porque no shared_ptr existe para destruirlo y desmantelarlo.

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 seguirse inmediatamente con la construcción del puntero inteligente correspondiente, sin ninguna otra operación intermedia. Esto implica que, para cuando el segundo new() se llama, se garantiza que el primer objeto ya haya sido envuelto en su puntero inteligente, evitando así cualquier fuga en caso de que se produzca una excepción.

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


13
2017-08-14 21:22