c++ - org - mapwindows 5
¿Es seguro reutilizar una ubicación de memoria? (8)
Dependiendo de la definición de
Data
, su código
puede
estar roto.
Es un
mal
código, de cualquier manera.
Si
Data
es un tipo de datos antiguo simple (POD, es decir, un typedef para un tipo básico, una estructura de tipos de POD, etc.),
y la memoria asignada está correctamente alineada para el tipo
(*), entonces su código está
bien definido
, lo que significa que "funcionará" (siempre que
inicialice
cada miembro de
*data_d
antes de usarlo), pero no es una buena práctica.
(Vea abajo.)
Si los
Data
no son de tipo POD, se dirige a problemas: la asignación del puntero no habría invocado ningún constructor, por ejemplo.
data_d
, que es de tipo "puntero a
Data
", estaría
mintiendo
efectivamente porque apunta a algo, pero ese algo
no es de tipo
Data
porque no se ha creado / construido / inicializado tal tipo.
El comportamiento indefinido no estará muy lejos en ese punto.
La solución para construir correctamente un objeto en una ubicación de memoria dada se llama colocación nueva :
Data * data_d = new (data) Data();
Esto le indica al compilador que construya un objeto de
Data
en los
data
ubicación
.
Esto funcionará para los tipos POD y no POD por igual.
También deberá llamar al destructor (
data_d->~Data()
) para asegurarse de que se ejecuta antes de
delete
la memoria.
Tenga cuidado de no mezclar nunca las funciones de asignación / liberación. Cualquier cosa que
malloc()
necesite serfree()
d, lo que se asigna connew
necesidades sedelete
, y si esnew []
tendrá quedelete []
. Cualquier otra combinación es UB.
En cualquier caso, se desaconseja el uso de punteros "desnudos" para la propiedad de la memoria en C ++. Usted debería
-
poner
new
en un constructor y ladelete
correspondiente en el destructor de una clase, haciendo que el objeto sea el propietario de la memoria (incluida la desasignación adecuada cuando el objeto sale del alcance, por ejemplo, en el caso de una excepción); o -
use un puntero inteligente que efectivamente haga lo anterior por usted.
(*): Se sabe que las implementaciones definen tipos "extendidos", cuyos requisitos de alineación no son tomados en cuenta por malloc ().
No estoy seguro si los abogados de idiomas todavía los llamarían "POD", en realidad.
MSVC, por ejemplo, realiza
una alineación de 8 bytes
en malloc () pero define el tipo extendido SSE
__m128
como que tiene un requisito de
alineación de 16 bytes
.
Esta pregunta se basa en algún código C existente portado a C ++. Solo me interesa saber si es "seguro". Ya sé que no lo habría escrito así. Soy consciente de que el código aquí es básicamente C en lugar de C ++, pero está compilado con un compilador de C ++ y sé que los estándares son ligeramente diferentes a veces.
Tengo una función que asigna algo de memoria.
Lanzo el
void*
devuelto
void*
a un
int*
y empiezo a usarlo.
Más tarde lanzo el
void*
devuelto
void*
a un
Data*
y empiezo a usarlo.
¿Es esto seguro en C ++?
Ejemplo:
void* data = malloc(10000);
int* data_i = (int*)data;
*data_i = 123;
printf("%d/n", *data_i);
Data* data_d = (Data*)data;
data_d->value = 456;
printf("%d/n", data_d->value);
Nunca leo las variables utilizadas a través de un tipo diferente al que se almacenaron, pero me preocupa que el compilador pueda ver que
data_i
y
data_d
son tipos diferentes y, por lo tanto, no pueden
data_d
legalmente entre sí y decidir reordenar mi código, por ejemplo, colocar la tienda en
data_d
antes del primera
printf
Lo que rompería todo.
Sin embargo, este es un patrón que se usa todo el tiempo.
Si inserta una
free
y
malloc
entre los dos accesos, no creo que altere nada, ya que no toca la memoria afectada y puede reutilizar los mismos datos.
¿Mi código está roto o es "correcto"?
Efectivamente, ha implementado su propio asignador encima de
malloc
/
free
que reutiliza un bloque en este caso.
Eso es perfectamente seguro.
Los envoltorios de asignación ciertamente pueden reutilizar bloques siempre que el bloque sea lo suficientemente grande y provenga de una fuente que garantice una alineación suficiente (y
malloc
hace).
Está "bien", funciona como lo ha escrito (suponiendo primitivas y tipos de datos simples (POD)). Es seguro. Es efectivamente un administrador de memoria personalizado.
Algunas notas:
-
Si se crean objetos con destructores no triviales en la ubicación de la memoria asignada, asegúrese de que se llame
obj->~obj();
-
Si crea objetos, considere la colocación de una nueva sintaxis sobre un molde simple (también funciona con POD)
Object* obj = new (data) Object();
-
Compruebe si hay un
nullptr
(oNULL
), simalloc
falla, se devuelveNULL
- La alineación no debería ser un problema, pero siempre tenga en cuenta al crear un administrador de memoria y asegúrese de que la alineación sea adecuada
Dado que está utilizando un compilador de C ++, a menos que desee mantener la naturaleza "C" en el código, también puede buscar el
operator new()
global
operator new()
.
Y como siempre, una vez hecho esto, no olvide el
free()
(o
delete
si usa
new
)
Menciona que todavía no va a convertir ninguno de los códigos;
pero si o si lo considera, hay algunas características idiomáticas en C ++ que puede desear usar sobre el
malloc
o incluso el global
::operator new
.
Debería buscar el puntero inteligente
std::unique_ptr<>
o
std::shared_ptr<>
y permitir que se encarguen de los problemas de administración de memoria.
Las reglas que rodean el alias estricto pueden ser bastante complicadas.
Un ejemplo de alias estricto es:
int a = 0;
float* f = reinterpret_cast<float*>(&a);
f = 0.3;
printf("%d", a);
Esta es una violación estricta de alias porque:
- La vida útil de las variables (y su uso) se superponen
- están interpretando el mismo recuerdo a través de dos "lentes" diferentes
Si no está haciendo ambas cosas al mismo tiempo, su código no viola el alias estricto.
En C ++, la vida útil de un objeto comienza cuando termina el constructor y se detiene cuando comienza el destructor.
En el caso de los tipos incorporados (sin destructor) o POD (destructor trivial), la regla es que su vida útil finaliza cada vez que se sobrescribe o libera la memoria.
Nota: esto es específicamente para admitir la escritura de administradores de memoria;
después de todo,
malloc
se escribe en C y el
operator new
se escribe en C ++ y se les permite explícitamente agrupar la memoria.
Usé lentes específicamente en lugar de tipos porque la regla es un poco más difícil.
C ++ generalmente usa
la tipificación nominal
: si dos tipos tienen un nombre diferente, son diferentes.
Si accede a un valor de tipo dinámico
T
como si fuera una
U
, está violando el alias.
Hay una serie de excepciones a esta regla:
- acceso por clase base
- en los POD, acceda como puntero al primer atributo
Y la regla más complicada está relacionada con la
union
donde C ++ se desplaza hacia
la tipificación estructural
: puede acceder a un fragmento de memoria a través de dos tipos diferentes, si solo accede a partes al comienzo de este fragmento de memoria en el que los dos tipos comparten una inicial común secuencia.
§9.2 / 18 Si una unión de diseño estándar contiene dos o más estructuras de diseño estándar que comparten una secuencia inicial común, y si el objeto de unión de diseño estándar actualmente contiene una de estas estructuras de diseño estándar, se permite inspeccionar la estructura común parte inicial de cualquiera de ellos. Dos estructuras de diseño estándar comparten una secuencia inicial común si los miembros correspondientes tienen tipos compatibles con el diseño y ninguno de los miembros es un campo de bits o ambos son campos de bits con el mismo ancho para una secuencia de uno o más miembros iniciales.
Dado:
-
struct A { int a; };
-
struct B: A { char c; double d; };
-
struct C { int a; char c; char* z; };
Dentro de una
union X { B b; C c; };
union X { B b; C c; };
puede acceder a
xba
,
xbc
y
xca
,
xcc
al mismo tiempo;
sin embargo, acceder a
xbd
(respectivamente
xcz
) es una violación del alias si el tipo almacenado actualmente no es
B
(respectivamente, no
C
).
Nota: informalmente, la tipificación estructural es como asignar el tipo a una tupla de sus campos (aplanarlos).
Nota:
char*
está específicamente exento de esta regla, puede ver cualquier pieza de memoria a través de
char*
.
En su caso, sin la definición de
Data
no puedo decir si la regla de "lentes" podría ser violada, sin embargo, dado que usted es:
-
sobrescribir memoria con
Data
antes de acceder a través deData*
-
no acceder a través de
int*
después
entonces cumple con la regla de por vida y, por lo tanto, no se produce ningún alias en lo que respecta al idioma.
Mientras la memoria se use para una sola cosa a la vez, es segura.
Básicamente, utiliza los datos asignados como una
union
.
Si desea usar la memoria para instancias de clases y no solo estructuras simples de estilo C o tipos de datos, debe recordar hacer una
colocación nueva
para "asignar" los objetos, ya que esto realmente llamará al constructor del objeto.
El destructor al que debe llamar explícitamente cuando haya terminado con el objeto, no puede
delete
.
Mientras los
Data
sigan siendo un POD, esto debería estar bien.
De lo contrario, tendría que cambiar a una ubicación nueva.
Sin embargo, pondría una afirmación estática para que esto no cambie durante la refactorización posterior
No encuentro ningún error al reutilizar el espacio de memoria.
Lo único que me importa es la referencia colgante.
Reutilizando el espacio de memoria como has dicho, creo que no tiene ningún efecto en el programa.
Puedes continuar con tu programación.
Pero siempre es preferible
free()
el espacio y luego asignarlo a otra variable.
Siempre que solo maneje los tipos "C", esto estaría bien.
Pero tan pronto como use las clases de C ++, tendrá problemas con la inicialización adecuada.
Si suponemos que los
Data
serían
std::string
por ejemplo, el código sería muy incorrecto.
El compilador realmente no puede mover la tienda a través de la llamada a
printf
, porque ese es un efecto secundario visible.
El resultado tiene que ser como si los efectos secundarios se produjeran en el orden que prescribe el programa.