tengo el siguiente código.
#include <iostream>
int * foo()
{
int a = 5;
return &a;
}
int main()
{
int* p = foo();
std::cout << *p;
*p = 8;
std::cout << *p;
}
¡Y el código se está ejecutando sin excepciones de tiempo de ejecución!
La salida fue 58
¿Cómo puede ser? ¿No es inaccesible la memoria de una variable local fuera de su función?
c++
memory-management
local-variables
dangling-pointer
desconocido
fuente
fuente
address of local variable ‘a’ returned
; espectáculos valgrindInvalid write of size 4 [...] Address 0xbefd7114 is just below the stack ptr
Respuestas:
Usted alquila una habitación de hotel. Pones un libro en el cajón superior de la mesita de noche y te duermes. Echa un vistazo a la mañana siguiente, pero "olvida" devolver la clave. ¡Robas la llave!
Una semana después, regresas al hotel, no te registras, te escabulles a tu antigua habitación con tu llave robada y miras en el cajón. Tu libro sigue ahí. ¡Asombroso!
¿Como puede ser? ¿No son inaccesibles los contenidos del cajón de una habitación de hotel si no ha alquilado la habitación?
Bueno, obviamente, ese escenario puede suceder en el mundo real sin problemas. No hay una fuerza misteriosa que haga que tu libro desaparezca cuando ya no estés autorizado para estar en la habitación. Tampoco hay una fuerza misteriosa que te impide entrar a una habitación con una llave robada.
La administración del hotel no está obligada a eliminar su libro. No hiciste un contrato con ellos que dijera que si dejas cosas atrás, lo destrozarán por ti. Si vuelve a ingresar ilegalmente a su habitación con una llave robada para recuperarla, no se requiere que el personal de seguridad del hotel lo atrape a escondidas. No hizo un contrato con ellos que dijera "si trato de colarse nuevamente más tarde, debes detenerme ". Por el contrario, firmó un contrato con ellos que decía "Prometo no volver a mi habitación más tarde", un contrato que rompió .
En esta situación puede pasar cualquier cosa . El libro puede estar allí, tienes suerte. El libro de otra persona puede estar allí y el suyo puede estar en el horno del hotel. Alguien podría estar allí justo cuando entras, rompiendo tu libro en pedazos. El hotel podría haber eliminado la mesa y el libro por completo y reemplazarlo con un armario. Todo el hotel podría estar a punto de ser demolido y reemplazado por un estadio de fútbol, y morirás en una explosión mientras te escabulles.
No sabes lo que va a pasar; cuando el check out del hotel y robaron una tecla para usar ilegalmente después, le dio el derecho a vivir en un mundo predecible, seguro, ya que optó por romper las reglas del sistema.
C ++ no es un lenguaje seguro . Alegremente te permitirá romper las reglas del sistema. Si intentas hacer algo ilegal y tonto como regresar a una habitación en la que no estás autorizado a entrar y hurgar en un escritorio que tal vez ya no esté allí, C ++ no te detendrá. Los lenguajes más seguros que C ++ resuelven este problema al restringir su poder, al tener un control mucho más estricto sobre las teclas, por ejemplo.
ACTUALIZAR
Santo cielo, esta respuesta está recibiendo mucha atención. (No estoy seguro de por qué, lo consideré solo una pequeña analogía "divertida", pero lo que sea).
Pensé que podría ser pertinente actualizar esto un poco con algunas ideas más técnicas.
Los compiladores están en el negocio de generar código que gestiona el almacenamiento de los datos manipulados por ese programa. Hay muchas formas diferentes de generar código para administrar la memoria, pero con el tiempo se han arraigado dos técnicas básicas.
El primero es tener algún tipo de área de almacenamiento de "larga vida" donde la "vida útil" de cada byte en el almacenamiento, es decir, el período de tiempo cuando está asociado de manera válida con alguna variable de programa, no se puede predecir fácilmente con anticipación de tiempo. El compilador genera llamadas en un "administrador de almacenamiento dinámico" que sabe cómo asignar dinámicamente el almacenamiento cuando es necesario y reclamarlo cuando ya no es necesario.
El segundo método es tener un área de almacenamiento "de corta duración" donde la vida útil de cada byte sea bien conocida. Aquí, las vidas siguen un patrón de "anidación". La más longeva de estas variables de corta duración se asignará antes que cualquier otra variable de corta duración, y se liberará al final. Las variables de vida más corta se asignarán después de las de vida más larga y se liberarán antes que ellas. La vida útil de estas variables de vida más corta está "anidada" dentro de la vida de las de vida más larga.
Las variables locales siguen el último patrón; cuando se ingresa un método, sus variables locales cobran vida. Cuando ese método llama a otro método, las variables locales del nuevo método cobran vida. Estarán muertos antes de que las variables locales del primer método estén muertas. El orden relativo de los comienzos y finales de las vidas de los almacenamientos asociados con las variables locales se puede calcular con anticipación.
Por esta razón, las variables locales generalmente se generan como almacenamiento en una estructura de datos de "pila", porque una pila tiene la propiedad de que lo primero que se empuje será lo último que se extraiga.
Es como si el hotel decidiera alquilar habitaciones secuencialmente, y no se puede retirar hasta que todos con un número de habitación superior al que usted haya retirado.
Así que pensemos en la pila. En muchos sistemas operativos, obtienes una pila por subproceso y la pila se asigna a un cierto tamaño fijo. Cuando llamas a un método, las cosas se colocan en la pila. Si luego pasa un puntero a la pila fuera de su método, como lo hace el póster original aquí, eso es solo un puntero al medio de un bloque de memoria de un millón de bytes completamente válido. En nuestra analogía, sales del hotel; cuando lo haces, acabas de salir de la habitación ocupada con el número más alto. Si nadie más se registra después de usted y regresa a su habitación ilegalmente, se garantiza que todas sus cosas aún estarán allí en este hotel en particular .
Usamos pilas para tiendas temporales porque son realmente baratas y fáciles. No se requiere una implementación de C ++ para usar una pila para el almacenamiento de locales; podría usar el montón. No lo hace, porque eso haría que el programa fuera más lento.
No se requiere una implementación de C ++ para dejar intacta la basura que dejaste en la pila para que puedas volver a buscarla más tarde ilegalmente; Es perfectamente legal que el compilador genere código que vuelva a cero todo en la "habitación" que acaba de desocupar. No lo hace porque, de nuevo, eso sería costoso.
No se requiere una implementación de C ++ para garantizar que cuando la pila se reduce lógicamente, las direcciones que solían ser válidas todavía se asignan a la memoria. La implementación puede decirle al sistema operativo "hemos terminado de usar esta página de pila ahora. Hasta que yo diga lo contrario, emita una excepción que destruya el proceso si alguien toca la página de pila previamente válida". Nuevamente, las implementaciones en realidad no hacen eso porque es lento e innecesario.
En cambio, las implementaciones le permiten cometer errores y salirse con la suya. La mayor parte del tiempo Hasta que un día algo realmente horrible sale mal y el proceso explota.
Esto es problemático Hay muchas reglas y es muy fácil romperlas accidentalmente. Ciertamente tengo muchas veces. Y lo que es peor, el problema a menudo solo aparece cuando se detecta que la memoria está corrupta en miles de millones de nanosegundos después de que ocurrió la corrupción, cuando es muy difícil averiguar quién la echó a perder.
Más idiomas seguros para la memoria resuelven este problema al restringir su potencia. En C # "normal" simplemente no hay forma de tomar la dirección de un local y devolverla o almacenarla para más adelante. Puede tomar la dirección de un local, pero el idioma está ingeniosamente diseñado para que sea imposible usarlo después de la vida útil de los fines locales. Para tomar la dirección de un local y devolverla, debe poner el compilador en un modo especial "inseguro" y poner la palabra "inseguro" en su programa, para llamar la atención sobre el hecho de que probablemente esté haciendo algo peligroso que podría estar rompiendo las reglas.
Para más lectura:
¿Qué pasa si C # permitió la devolución de referencias? Casualmente, ese es el tema de la publicación del blog de hoy:
https://ericlippert.com/2011/06/23/ref-returns-and-ref-locals/
¿Por qué usamos pilas para administrar la memoria? ¿Los tipos de valor en C # siempre se almacenan en la pila? ¿Cómo funciona la memoria virtual? Y muchos más temas sobre cómo funciona el administrador de memoria C #. Muchos de estos artículos también están relacionados con los programadores de C ++:
https://ericlippert.com/tag/memory-management/
fuente
Lo que estás haciendo aquí es simplemente leer y escribir en la memoria que solía ser la dirección
a
. Ahora que está fuerafoo
, es solo un puntero a un área de memoria aleatoria. Sucede que, en su ejemplo, esa área de memoria existe y nada más la está usando en este momento. No se rompe nada al continuar usándolo, y nada más lo ha sobrescrito todavía. Por lo tanto, el5
sigue ahí. En un programa real, esa memoria se reutilizaría casi de inmediato y se rompería algo al hacer esto (¡aunque los síntomas pueden no aparecer hasta mucho más tarde!)Cuando regresa
foo
, le dice al sistema operativo que ya no está usando esa memoria y que puede reasignarse a otra cosa. Si tienes suerte y nunca se reasigna, y el sistema operativo no te atrapa usándolo de nuevo, entonces te saldrás con la mentira. Lo más probable es que termines escribiendo sobre cualquier otra cosa que termine con esa dirección.Ahora, si te estás preguntando por qué el compilador no se queja, probablemente sea porque
foo
fue eliminado por la optimización. Por lo general, te advertirá sobre este tipo de cosas. C asume que sabe lo que está haciendo, y técnicamente no ha violado el alcance aquí (no hay referencia aa
sí mismo fuera defoo
), solo las reglas de acceso a la memoria, que solo activan una advertencia en lugar de un error.En resumen: esto no suele funcionar, pero a veces lo hará por casualidad.
fuente
Porque el espacio de almacenamiento aún no estaba pisoteado. No cuentes con ese comportamiento.
fuente
Una pequeña adición a todas las respuestas:
si haces algo así:
la salida probablemente será: 7
Esto se debe a que después de regresar de foo (), la pila se libera y luego se reutiliza por boo (). Si desmonta el ejecutable, lo verá claramente.
fuente
boo
reutiliza lafoo
pila? no son pilas de funciones separadas entre sí, también recibo basura al ejecutar este código en Visual Studio 2015foo()
, existe, luego desciende aboo()
.Foo()
yBoo()
ambos ingresan con el puntero de la pila en la misma ubicación. Sin embargo, este no es un comportamiento en el que se deba confiar. Otro 'cosas' (como interrupciones, o el sistema operativo) pueden utilizar la pila entre la llamada deboo()
yfoo()
, modificando su contenido ...En C ++, puede acceder a cualquier dirección, pero eso no significa que deba hacerlo . La dirección a la que accede ya no es válida. Se trabaja porque nada más revueltos de la memoria después de foo regresó, pero podría estrellarse en muchas circunstancias. Intente analizar su programa con Valgrind , o incluso compilarlo optimizado, y vea ...
fuente
Nunca lanza una excepción de C ++ accediendo a memoria no válida. Solo está dando un ejemplo de la idea general de hacer referencia a una ubicación de memoria arbitraria. Podría hacer lo mismo así:
Aquí simplemente estoy tratando 123456 como la dirección de un doble y le escribo. Pueden suceder muchas cosas:
q
de hecho, podría ser una dirección válida de un doble, por ejemplodouble p; q = &p;
.q
podría apuntar a algún lugar dentro de la memoria asignada y simplemente sobrescribo 8 bytes allí.q
apunta fuera de la memoria asignada y el administrador de memoria del sistema operativo envía una señal de falla de segmentación a mi programa, lo que hace que el tiempo de ejecución lo finalice.La forma en que lo configura es un poco más razonable que la dirección devuelta apunte a un área válida de memoria, ya que probablemente estará un poco más abajo en la pila, pero sigue siendo una ubicación no válida a la que no puede acceder en un moda determinista
Nadie verificará automáticamente la validez semántica de las direcciones de memoria como esa durante la ejecución normal del programa. Sin embargo, un depurador de memoria como lo
valgrind
hará felizmente, por lo que debe ejecutar su programa a través de él y presenciar los errores.fuente
4) I win the lottery
¿Compiló su programa con el optimizador habilitado? La
foo()
función es bastante simple y podría haber sido incorporada o reemplazada en el código resultante.Pero estoy de acuerdo con Mark B en que el comportamiento resultante no está definido.
fuente
5
se cambiará ...Su problema no tiene nada que ver con el alcance . En el código que muestra, la función
main
no ve los nombres en la funciónfoo
, por lo que no puede accedera
en foo directamente con este nombre afuerafoo
.El problema que tiene es por qué el programa no señala un error al hacer referencia a memoria ilegal. Esto se debe a que los estándares C ++ no especifican un límite muy claro entre la memoria ilegal y la memoria legal. Hacer referencia a algo en la pila emergente a veces causa error y otras no. Depende. No cuentes con este comportamiento. Suponga que siempre generará un error cuando programe, pero suponga que nunca indicará un error cuando depure.
fuente
Solo está devolviendo una dirección de memoria, está permitido pero probablemente sea un error.
Sí, si intenta desreferenciar esa dirección de memoria, tendrá un comportamiento indefinido.
fuente
cout
.*a
apunta a memoria no asignada (liberada). Incluso si no lo abandonas, sigue siendo peligroso (y probablemente falso).Ese es un comportamiento indefinido clásico que se discutió aquí no hace dos días: busque un poco en el sitio. En pocas palabras, tuvo suerte, pero cualquier cosa podría haber sucedido y su código está haciendo un acceso no válido a la memoria.
fuente
Este comportamiento es indefinido, como señaló Alex; de hecho, la mayoría de los compiladores advertirán contra hacerlo, porque es una manera fácil de obtener bloqueos.
Para ver un ejemplo del tipo de comportamiento espeluznante que es probable que tenga, pruebe este ejemplo:
Esto imprime "y = 123", pero sus resultados pueden variar (¡de verdad!). Su puntero está golpeando otras variables locales no relacionadas.
fuente
Presta atención a todas las advertencias. No solo resuelva errores.
GCC muestra esta advertencia
Este es el poder de C ++. Deberías preocuparte por la memoria. Con la
-Werror
bandera, esta advertencia se convierte en un error y ahora debe depurarla.fuente
Funciona porque la pila no ha sido alterada (todavía) desde que se colocó allí. Llame a algunas otras funciones (que también están llamando a otras funciones) antes de acceder
a
nuevamente y probablemente ya no tenga tanta suerte ... ;-)fuente
En realidad invocaste un comportamiento indefinido.
Devolver la dirección de un trabajo temporal, pero como los temporales se destruyen al final de una función, los resultados de acceder a ellos serán indefinidos.
Por lo tanto, no modificó
a
sino la ubicación de la memoria dondea
alguna vez estuvo. Esta diferencia es muy similar a la diferencia entre estrellarse y no estrellarse.fuente
En las implementaciones típicas del compilador, puede pensar en el código como "imprimir el valor del bloque de memoria con la dirección que solía estar ocupada por un". Además, si agrega una nueva invocación de función a una función que mantiene un local
int
, es muy probable que cambie el valor dea
(o la dirección de memoria quea
solía señalar). Esto sucede porque la pila se sobrescribirá con un nuevo marco que contiene datos diferentes.Sin embargo, este es un comportamiento indefinido y no debe confiar en que funcione.
fuente
a
, el puntero contenía la dirección dea
. Aunque el Estándar no requiere que las implementaciones definan el comportamiento de las direcciones después de que la vida útil de su destino haya finalizado, también reconoce que en algunas plataformas UB se procesa de manera documentada, característica del entorno. Si bien la dirección de una variable local generalmente no será de mucha utilidad después de que se haya salido del alcance, algunos otros tipos de direcciones aún pueden ser significativos después de la vida útil de sus objetivos respectivos.realloc
se compare con el valor de retorno, ni que los punteros a direcciones dentro del bloque antiguo se ajusten para apuntar al nuevo, algunas implementaciones lo hacen , y el código que explota dicha característica puede ser más eficiente que el código que debe evitar cualquier acción, incluso comparaciones, que impliquen punteros a la asignación que se le asignórealloc
.Puede, porque
a
es una variable asignada temporalmente para la vida útil de su alcance (foo
función). Después de que regrese defoo
la memoria es libre y se puede sobrescribir.Lo que estás haciendo se describe como un comportamiento indefinido . El resultado no se puede predecir.
fuente
Las cosas con salida de consola correcta (?) Pueden cambiar dramáticamente si usa :: printf pero no cout. Puede jugar con el depurador dentro del código siguiente (probado en x86, 32 bits, MSVisual Studio):
fuente
Después de regresar de una función, todos los identificadores se destruyen en lugar de los valores guardados en una ubicación de memoria y no podemos ubicar los valores sin tener un identificador. Pero esa ubicación aún contiene el valor almacenado por la función anterior.
Entonces, aquí la función
foo()
está devolviendo la dirección dea
ya
se destruye después de devolver su dirección. Y puede acceder al valor modificado a través de esa dirección devuelta.Déjame tomar un ejemplo del mundo real:
Supongamos que un hombre esconde dinero en una ubicación y le dice la ubicación. Después de un tiempo, el hombre que le había dicho la ubicación del dinero muere. Pero aún así tienes acceso a ese dinero escondido.
fuente
Es una forma "sucia" de usar direcciones de memoria. Cuando devuelve una dirección (puntero) no sabe si pertenece al ámbito local de una función. Es solo una dirección. Ahora que invocó la función 'foo', esa dirección (ubicación de memoria) de 'a' ya estaba asignada allí en la memoria direccionable (segura, por ahora al menos) de su aplicación (proceso). Después de que la función 'foo' regresó, la dirección de 'a' puede considerarse 'sucia' pero está allí, no limpiada, ni alterada / modificada por expresiones en otra parte del programa (al menos en este caso específico). El compilador de AC / C ++ no le impide tener un acceso tan 'sucio' (sin embargo, podría advertirle si le importa).
fuente
Tu código es muy arriesgado. Está creando una variable local (que se considera destruida después de que finaliza la función) y devuelve la dirección de memoria de esa variable después de que se destruye.
Eso significa que la dirección de memoria podría ser válida o no, y su código será vulnerable a posibles problemas de dirección de memoria (por ejemplo, falla de segmentación).
Esto significa que está haciendo algo muy malo, porque está pasando una dirección de memoria a un puntero que no es confiable en absoluto.
Considere este ejemplo, en cambio, y pruébelo:
A diferencia de su ejemplo, con este ejemplo usted es:
fuente
new
.new
. Les estás enseñando a usarnew
. Pero no deberías usarlonew
.new
en 2019 (a menos que esté escribiendo código de biblioteca) y tampoco enseñe a los recién llegados a hacerlo! Salud.