¿Qué hay de malo en usar arreglos asignados dinámicamente en C++? [duplicate]

10 minutos de lectura

avatar de usuario
Espuela de plata

Como el siguiente código:

int size = myGetSize();
std::string* foo;
foo = new std::string[size];
//...
// using the table
//...
delete[] foo;

Escuché que dicho uso (no este código precisamente, sino la asignación dinámica en su conjunto) puede ser inseguro en algunos casos y debe usarse solo con RAII. ¿Por qué?

  • Digamos que se lanza una excepción antes de la llamada a delete[]. Entonces usted tiene comportamiento indefinido. También, foo no contiene información re. a qué apunta (¿es un puntero a un string? a una matriz de strings? Deber delete ¿ser llamado? ¿O debería alguien más hacer eso?

    – juanchopanza

    10 de junio de 2014 a las 8:21


  • Por qué comportamiento indefinido? ¿No es ‘solo’ una pérdida de memoria?

    – alain

    10 de junio de 2014 a las 8:23

  • @juanchopanza: no, no UB. es simplemente una pérdida de memoria. por ejemplo, una práctica común (ya veces necesaria) para los singletons es asignarlos dinámicamente y nunca destruirlos.

    – Saludos y hth. – alf

    10 de junio de 2014 a las 8:30

  • @MatthieuM.: capítulo y verso por favor

    – Saludos y hth. – alf

    10 de junio de 2014 a las 8:31

  • @MatthieuM.: Me sorprende verte categorizando pérdida de memoria como comportamiento indefinido. No, no es. Aunque UB puede que causar pérdida de memoria (como eliminar el puntero de clase base cuyo destructor no está marcado virtual), pero la mera fuga de memoria no invoca UB.

    – Nawaz

    10 de junio de 2014 a las 8:58


avatar de usuario
Kerrek SB

Veo tres problemas principales con su código:

  1. Uso de punteros desnudos y propietarios.

  2. uso de desnudo new.

  3. Uso de arreglos dinámicos.

Cada uno es indeseable por sus propias razones. Trataré de explicar cada uno a su vez.

(1) viola lo que me gusta llamar corrección de subexpresionesy (2) viola corrección de declaraciones. La idea aquí es que ninguna declaración, y ni siquiera cualquier subexpresión, debería ser por sí mismo un error. Tomo el término “error” vagamente en el sentido de “podría ser un error”.

La idea de escribir un buen código es que si sale mal, no fue tu culpa. Tu mentalidad básica debe ser la de un cobarde paranoico. No escribir código en absoluto es una forma de lograr esto, pero dado que rara vez cumple con los requisitos, lo mejor es asegurarse de que hagas lo que hagas, no sea tu culpa. La única forma en que puede probar sistemáticamente que no es su culpa es si nadie parte de su código es la causa raíz de un error. Ahora veamos el código de nuevo:

  • new std::string[25] es un error, porque crea un objeto asignado dinámicamente que se filtra. Este código solo puede convertirse condicionalmente en un error si alguien más, en otro lugar, y en todos los casos, recuerda limpiar.

    Esto requiere, en primer lugar, que el valor de esta expresión se almacene en algún lugar. Esto está sucediendo en su caso, pero en expresiones más complejas puede ser difícil demostrar que alguna vez sucederá en todos los casos (orden de evaluación no especificado, lo estoy mirando).

  • foo = new std::string[125]; es un error porque de nuevo foo filtra un recurso, a no ser que los astros se alinean y alguien se acuerda, en cada caso y en el momento oportuno, de limpiar.

La forma correcta de escribir este código hasta ahora sería:

std::unique_ptr<std::string[]> foo(std::make_unique<std::string[]>(25));

Tenga en cuenta que cada subexpresión en esta declaración no es la causa raíz de un error de programa. No es tu culpa.

Finalmente, en cuanto a (3), las matrices dinámicas son una característica incorrecta en C++ y básicamente nunca deberían usarse. Hay varios defectos estándar relacionados solo con matrices dinámicas (y no se considera que valga la pena corregirlos). El argumento simple es que no puede usar matrices sin conocer su tamaño. Podría decir que podría usar un valor centinela o una lápida para marcar el final de una matriz dinámicamente, pero eso hace que su programa sea correcto. valor-dependiente, no escribe-dependiente y, por lo tanto, no verificable estáticamente (la definición misma de “inseguro”). No puede afirmar estáticamente que no fue su culpa.

De todos modos, termina teniendo que mantener un almacenamiento separado para el tamaño de la matriz. Y adivina qué, tu implementación tiene que duplicar ese conocimiento de todos modos para que pueda llamar a los destructores cuando digas delete[], por lo que es una duplicación desperdiciada. La forma correcta, en cambio, es no usar matrices dinámicas, sino separar la asignación de memoria (y hacerla personalizable a través de los asignadores por qué estamos en eso) de la construcción de objetos por elementos. Envolver todo esto (asignador, almacenamiento, conteo de elementos) en una sola clase conveniente es la forma de C++.

Por lo tanto, la versión final de su código es esta:

std::vector<std::string> foo(25);

  • Nota: hubo una propuesta std::dynarray clase (que fue puesta en espera o rechazada). Algunas personas argumentan que std::vector almacena un miembro de capacidad adicional y tiene capacidades de cambio de tamaño que no son necesarias en varios casos y debería existir una versión reducida (sin cambio de tamaño).

    – Matthieu M.

    10 de junio de 2014 a las 8:44

  • @MatthieuM.: Si está en Itanium ABI, vector es todavía mejor que una matriz dinámica cuando tienes destructores. Sin embargo, estoy de acuerdo en que falta una matriz agradable, dinámica y de tamaño fijo. dynarray no era exactamente lo correcto (creo que ahora está en un TS experimental). Boost probablemente tiene algo apropiado.

    – KerrekSB

    10 de junio de 2014 a las 8:46


  • Tenga en cuenta que std::make_unique aún no forma parte del estándar C++ (a partir de C++11).

    – Saludos y hth. – alf

    10 de junio de 2014 a las 9:01

  • Re “Finalmente, en cuanto a (3), las matrices dinámicas son una característica incorrecta en C ++ y básicamente nunca deben usarse”, ese es un consejo demasiado absoluto. Dentro del mundo de los lenguajes de programación, algunos tienen que usar C++ para crear cosas que otros usan. Y de manera similar dentro de C ++, algunos tienen que usar matrices dinámicas y colocar programación TMP nueva e inabarcable, etc. para crear las cosas que otros usan. Cuando el C++ permitido se reduce a un subconjunto seguro como C#, ¿por qué no usar C# en su lugar? O Java, lo que sea. Pero incluso esos lenguajes no son seguros para muchos programadores. Pronto…

    – Saludos y hth. – alf

    10 de junio de 2014 a las 9:07

  • @Alf ¿Puede señalar un uso válido de matriz nueva? (Supongo que eso es lo que quiso decir con “matrices dinámicas”.) He estado escribiendo C ++ durante unos 25 años, incluida la implementación de contenedores preestándar a lo largo de las líneas de cadena y vector, y nunca he encontrado uno.

    – James Kanze

    10 de junio de 2014 a las 11:39

El código que propone no es seguro para excepciones, y la alternativa:

std::vector<std::string> foo( 125 );
//  no delete necessary

es. Y por supuesto, el vector conoce el tamaño más tarde y puede realizar comprobaciones de límites en el modo de depuración; se puede pasar (por referencia o incluso por valor) a una función, que luego podrá usarlo, sin ningún argumento adicional. Array new sigue las convenciones de C para arreglos, y los arreglos en C están seriamente dañados.

Por lo que puedo ver, hay nunca un caso donde una matriz new es apropiada.

avatar de usuario
utnapistim

Escuché que dicho uso (no este código precisamente, sino la asignación dinámica en su conjunto) puede ser inseguro en algunos casos y debe usarse solo con RAII. ¿Por qué?

Toma este ejemplo (similar al tuyo):

int f()
{
    char *local_buffer = new char[125];
    get_network_data(local_buffer);
    int x = make_computation(local_buffer);
    delete [] local_buffer;
    return x;
}

Esto es trivial.

Incluso si escribe correctamente el código anterior, alguien puede venir un año después y agregar un condicional, o diez o veinte, en su función:

int f()
{
    char *local_buffer = new char[125];
    get_network_data(local_buffer);
    int x = make_computation(local_buffer);
    if(x == 25)
    {
        delete[] local_buffer;   
        return 2;
    }
    if(x < 0)
    {
        delete[] local_buffer; // oops: duplicated code
        return -x;
    }
    if(x || 4)
    {
        return x/4; // oops: developer forgot to add the delete line
    }
    delete[] local_buffer; // triplicated code
    return x;
}

Ahora, asegurarse de que el código no tenga fugas de memoria es más complicado: tiene múltiples rutas de código y cada una de ellas tiene que repetir la declaración de eliminación (e introduje una fuga de memoria a propósito, para darle un ejemplo).

Esto es todavía un caso trivial, con un solo recurso (local_buffer), y (ingenuamente) asume que el código no genera excepción alguna, entre la asignación y la desasignación. El problema conduce a un código que no se puede mantener, cuando su función asigna ~10 recursos locales, puede lanzar y tiene múltiples rutas de retorno.

Más que eso, la progresión anterior (caso simple y trivial extendido a una función más compleja con múltiples rutas de salida, extendido a múltiples recursos, etc.) es una progresión natural del código en el desarrollo de la mayoría de los proyectos. No usar RAII crea una forma natural para que los desarrolladores actualicen el código, de una manera que disminuirá la calidad, durante la vida útil del proyecto (esto se llama cruft, y es una cosa muy mala).

TLDR: el uso de punteros sin procesar en C ++ para la administración de memoria es una mala práctica (aunque para implementar un rol de observador, una implementación con punteros sin procesar está bien). La gestión de recursos con punteros sin procesar infringe PVP y SECO principios).

  • +1 por mencionar algunas cosas que debería haber mencionado pero olvidé

    – Saludos y hth. – alf

    10 de junio de 2014 a las 10:39

Hay dos desventajas principales:

  1. new no garantiza que la memoria que está asignando se inicialice con 0s o null. Tendrán valores indefinidos a menos que los inicialice.

  2. En segundo lugar, la memoria se asigna dinámicamente, lo que significa que está alojada en heap no en stack. La diferencia entre heap y stack es que las pilas se borran cuando la variable se queda fuera del alcance pero heapLos mensajes de correo electrónico no se borran automáticamente y además C++ no contiene un recolector de basura integrado, lo que significa, en su caso, cómo delete se perdió la llamada, terminó con una pérdida de memoria.

el puntero en bruto es difícil de manejar correctamente, por ejemplo, wrt. copia de objetos.

es mucho más simple y seguro usar una abstracción bien probada como std::vector.

en resumen, no reinvente la rueda innecesariamente: otros ya han creado algunas ruedas excelentes que probablemente no igualará en calidad o precio.

avatar de usuario
usuario3722371

Si la memoria asignada no se libera cuando ya no es necesaria, se producirá una fuga de memoria. No se especifica qué sucederá con la memoria filtrada, pero los sistemas operativos contemporáneos la recopilan cuando finaliza el programa. Las fugas de memoria pueden ser muy peligrosas porque el sistema puede quedarse sin memoria.

avatar de usuario
Codor

los delete al final se puede saltar. El código que se muestra no es “incorrecto” en el sentido más estricto, pero C ++ ofrece administración automática de memoria para variables tan pronto como se abandona su alcance; usar un puntero no es necesario en su ejemplo.

¿Ha sido útil esta solución?