pointer - structur c++
¿Qué hay de malo con el uso de matrices asignadas dinámicamente en C++? (9)
Escuché que ese uso (no este código de manera precisa, pero la asignación dinámica en su conjunto) puede ser inseguro en algunos casos, y se debe usar 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 escribes el código arriba correctamente, alguien puede venir un año después y agregar un condicional, o diez o veinte, a tu 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 pérdidas de memoria es más complicado: tiene varias rutas de código y cada una de ellas tiene que repetir la instrucción de eliminación (y presenté una pérdida de memoria a propósito, para darle un ejemplo).
Este es aún un caso trivial, con un solo recurso (local_buffer), y (ingenuamente) supone que el código no genera excepciones entre la asignación y la desasignación. El problema lleva a un código que no se puede mantener, cuando su función asigna unos 10 recursos locales, puede lanzar y tiene múltiples rutas de retorno.
Más que eso, la progresión anterior (caso simple, 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 manera que disminuya la calidad, durante la vida útil del proyecto ( esto se llama cruft y es una cosa muy mala ).
TLDR: el uso de punteros sin formato en C ++ para la gestión de la memoria es una mala práctica (aunque para implementar un rol de observador, una implementación con punteros sin formato, está bien). La gestión de recursos con puntos en bruto viola los principios de SRP y DRY ).
Esta pregunta ya tiene una respuesta aquí:
Como el siguiente código:
int size = myGetSize();
std::string* foo;
foo = new std::string[size];
//...
// using the table
//...
delete[] foo;
Escuché que ese uso (no este código de manera precisa, pero la asignación dinámica en su conjunto) puede ser inseguro en algunos casos, y se debe usar solo con RAII. ¿Por qué?
El código que usted propone no es seguro para las 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 adelante 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á usarla, sin ningún argumento adicional. La nueva matriz sigue las convenciones de C para las matrices, y las matrices en C están seriamente dañadas.
Por lo que puedo ver, nunca hay un caso en el que una matriz nueva sea apropiada.
Hay dos inconvenientes principales de esto:
new
no garantiza que la memoria que está asignando se inicialice con0
s onull
. Tendrán valores indefinidos a menos que los inicialice.En segundo lugar, la memoria se asigna dinámicamente, lo que significa que se aloja en el
heap
no en lastack
. La diferencia entre elheap
y lastack
es que, las pilas se borran cuando la variable se queda fuera del alcance pero los montones no se borran automáticamente y, además, C ++ no contiene un recolector de basura incorporado, lo que significa que si alguna vez se pierde la llamada dedelete
Terminó con una pérdida de memoria.
La delete
al final podría ser omitida. El código mostrado no es "incorrecto" en el sentido más estricto, pero C ++ ofrece administración automática de la memoria para las variables tan pronto como se deja su alcance; usar un puntero no es necesario en tu ejemplo.
Si la memoria asignada no se libera cuando ya no es necesaria, se producirá una pérdida de memoria. No se especifica qué sucederá con la memoria filtrada, pero los sistemas operativos contemporáneos la recopilan cuando el programa termina. Las pérdidas de memoria pueden ser muy peligrosas porque el sistema puede quedarse sin memoria.
Tenga la asignación dentro de un bloque try y el bloque catch debe desasignar toda la memoria asignada hasta el momento y también en la salida normal fuera del bloque de excepción, y el bloque catch no debe pasar por el bloque de ejecución normal para evitar la eliminación doble
Veo tres problemas principales con su código:
Uso de punteros desnudos, poseedores.
Uso de desnudos
new
.Uso de matrices dinámicas.
Cada uno es indeseable por sus propias razones. Intentaré explicar cada uno a su vez.
(1) viola lo que me gusta llamar corrección de subexpresión , y (2) viola corrección de declaración . La idea aquí es que ninguna declaración, y ni siquiera una subexpresión , debería ser por sí misma un error. Tomo el término "error" para decir "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 debería ser la de un cobarde paranoico. No escribir código en absoluto es una forma de lograrlo, pero dado que rara vez cumple con los requisitos, lo mejor que puede hacer es asegurarse de que, sea lo que sea que haga, no sea su culpa. La única forma en que puede probar sistemáticamente que no es su culpa es si ninguna parte de su código es la causa principal 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 no error si alguien más, en algún 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 probar que alguna vez sucederá en todos los casos (orden de evaluación no especificada, lo estoy viendo).
foo = new std::string[125];
es un error porque nuevamentefoo
filtra un recurso, a menos que las estrellas se alineen y alguien recuerde, en cada caso y en el momento adecuado, 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 errónea en C ++ y, básicamente, nunca deberían usarse. Hay varios defectos estándar relacionados con arreglos dinámicos (y no se considera que valgan la pena arreglarlos). El argumento simple es que no puedes usar matrices sin saber su tamaño. Podría decir que podría usar un valor de centinela o piedra sepulcral para marcar el final de una matriz dinámicamente, pero eso hace que la corrección del valor de su programa sea dependiente, no de tipo , y por lo tanto no sea verificable estáticamente (la definición misma de "inseguro "). No puede afirmar estáticamente que no fue su culpa.
Entonces, de todos modos, tendrás que mantener un almacenamiento separado para el tamaño del arreglo. Y adivina qué, tu implementación tiene que duplicar ese conocimiento de todos modos para que pueda llamar a los destructores cuando dices delete[]
, así que eso es desperdicio de duplicación. La forma correcta, en su lugar, es no usar matrices dinámicas, sino una asignación de memoria separada (y hacerla personalizable a través de los asignadores por los que estamos en ello) de la construcción de objetos por elementos. Todo esto (asignador, almacenamiento, recuento de elementos) en una sola clase conveniente es la forma en C ++.
Así, la versión final de tu código es esta:
std::vector<std::string> foo(25);
Ver estándares de codificación JPL . La asignación de memoria dinámica conduce a una ejecución impredecible. He visto problemas en las asignaciones de memoria dinámica en sistemas perfectamente codificados, que con el tiempo, existe una fragmentación de la memoria como un disco duro. La asignación de bloques de memoria del montón llevará más tiempo, hasta que sea imposible asignar el tamaño solicitado. En ese momento, comienza a obtener que se devuelvan los punteros NULL y todo el programa se bloquea porque pocos, si no nadie, comprueban las condiciones de falta de memoria. Es importante tener en cuenta que, según el libro, puede tener suficiente memoria disponible, sin embargo, la fragmentación del mismo es lo que impide la asignación. Esto se trata en .NET CLI, con el uso de "manejadores" en lugar de punteros , donde el motor de ejecución puede recolectar la basura, usando un recolector de basura de marca y barrido, mover la memoria. Durante el barrido, compacta la memoria para evitar la fragmentación y actualiza los controladores. Mientras que los punteros (direcciones de memoria) no se pueden actualizar. Sin embargo, esto es un problema, porque la recolección de basura ya no es determinista. Sin embargo, .NET ha agregado mecanismos para hacerlo más determinista. Sin embargo, si sigue los consejos de JPL (sección 2.5), no necesita una recolección de basura elegante. Asigna dinámicamente todo lo que necesita en la inicialización, luego reutiliza la memoria asignada, nunca la libera, entonces no existe riesgo de fragmentación y aún puede tener una recolección de basura determinista.
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 innecesariamente la rueda: otras ya han creado unas ruedas excelentes que no es probable que coincidan en calidad o precio.