c++ - tipos - variables globales y locales en visual basic




¿Se puede acceder a la memoria de una variable local fuera de su alcance? (13)

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 de 58

¿Cómo puede ser? ¿No es inaccesible la memoria de una variable local fuera de su función?


¿Cómo puede ser? ¿No es inaccesible la memoria de una variable local fuera de su función?

Usted alquila una habitación de hotel. Usted pone un libro en el cajón superior de la mesita de noche y se va a dormir. Echa un vistazo a la mañana siguiente, pero "olvida" devolver tu llave. ¡Robas la llave!

Una semana más tarde, regresa al hotel, no se registra, entra a escondidas en su antigua habitación con la llave robada y mira el cajón. Su libro todavía está allí. ¡Asombroso!

¿Como puede ser? ¿No se puede acceder al contenido de un cajón de la habitación de hotel si no ha alquilado la habitación?

Bueno, obviamente ese escenario puede ocurrir en el mundo real, no hay problema. 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 impida entrar en 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 destruirán por ti. Si vuelves a entrar ilegalmente a tu habitación con una llave robada para recuperarla, no es necesario que el personal de seguridad del hotel te atrape furtivamente. No hiciste un contrato con ellos que dijera "si trato de volver a colarse en mi habitación". Habitación más tarde, estás obligado a detenerme ". Más bien, firmó un contrato con ellos que decía "Prometo no volver a escabullirme a mi habitación más tarde", un contrato que usted rompió .

En esta situación puede pasar cualquier cosa . El libro puede estar ahí, tienes suerte. El libro de otra persona puede estar allí y el suyo podría estar en el horno del hotel. Alguien podría estar allí justo cuando entras, haciendo pedazos tu libro. El hotel podría haber retirado 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 usted morirá en una explosión mientras se escabulle.

No sabes lo que va a pasar; cuando se retiró del hotel y robó una llave para usarla ilegalmente más tarde, renunció al derecho a vivir en un mundo predecible y seguro porque eligió romper las reglas del sistema.

C ++ no es un lenguaje seguro . Con alegría le permitirá romper las reglas del sistema. Si intentas hacer algo ilegal y tonto, como volver a una habitación en la que no estás autorizado a entrar y hurgar en un escritorio que podría no estar más allí, C ++ no te detendrá. Los idiomas más seguros que C ++ resuelven este problema restringiendo su poder, por ejemplo, al tener un control mucho más estricto sobre las claves.

ACTUALIZAR

Dios santo, esta respuesta está recibiendo mucha atención. (No estoy seguro de por qué: lo consideré solo como una pequeña analogía "divertida", pero como sea).

Pensé que podría ser pertinente actualizar esto un poco con algunas ideas técnicas más.

Los compiladores están en el negocio de generar código que gestione el almacenamiento de los datos manipulados por ese programa. Existen muchas formas diferentes de generar código para administrar la memoria, pero con el tiempo dos técnicas básicas se han afianzado.

El primero es tener algún tipo de área de almacenamiento "de larga duración" en la que la "vida útil" de cada byte en el almacenamiento, es decir, el período de tiempo en que se asocia de manera válida con alguna variable del programa, no se pueda predecir fácilmente. de tiempo. El compilador genera llamadas a 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” en la que se conozca la vida útil de cada byte. Aquí, las vidas siguen un patrón de "anidación". La vida más larga de estas variables de corta duración se asignará antes que cualquier otra variable de corta duración, y se liberará en último lugar. Las variables de vida más corta se asignarán después de las de mayor duración y se liberarán antes que ellas. La vida útil de estas variables de vida más corta está "anidada" dentro de la vida útil de las variables 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 la vida útil de los almacenes asociados con las variables locales se puede establecer 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 presione será lo último que se desprenda.

Es como si el hotel decidiera alquilar habitaciones solo secuencialmente, y usted no puede irse hasta que todos los que tengan un número de habitación más alto que el que ha hecho la salida.

Así que pensemos en la pila. En muchos sistemas operativos, se obtiene una pila por subproceso y la pila se asigna a un determinado 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 aquí el póster original, 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, te retiras del hotel; cuando lo hace, acaba 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 usted regresa a su habitación ilegalmente, se garantiza que todas sus cosas seguirá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 la basura que dejaste en la pila sin tocar, de modo que puedas volver por ella ilegalmente más tarde; es perfectamente legal que el compilador genere un código que haga que todo vuelva a cero en la "sala" que acaba de desocupar. No es porque una vez más, eso sería caro.

No se requiere una implementación de C ++ para garantizar que cuando la pila se contrae de manera lógica, 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 diga lo contrario, emita una excepción que destruya el proceso si alguien toca la página de pila que era válida anteriormente". Nuevamente, las implementaciones no lo hacen 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 terrible 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, a menudo el problema solo surge cuando se detecta que la memoria está dañada miles de millones de nanosegundos después de que ocurrió la corrupción, cuando es muy difícil averiguar quién la arruinó.

Más lenguajes seguros para la memoria resuelven este problema restringiendo su poder. En C # "normal" simplemente no hay forma de tomar la dirección de un local y devolverla o almacenarla para más tarde. Puede tomar la dirección de un local, pero el idioma está diseñado inteligentemente 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 leer más:


¿Compiló su programa con el optimizador habilitado?

La función foo () es bastante simple y podría haber sido incorporada / reemplazada en el código resultante.

Pero estoy de acuerdo con Mark B en que el comportamiento resultante no está definido.


En C ++, puedes acceder a cualquier dirección, pero eso no significa que debas hacerlo . La dirección a la que está accediendo ya no es válida. Funciona porque nada más revolvió la memoria después de que regresara Foo, pero podría fallar en muchas circunstancias. Intente analizar su programa con Valgrind , o incluso simplemente compilándolo optimizado, y vea ...


En las implementaciones típicas de compiladores, puede pensar en el código como "imprimir el valor del bloque de memoria con una 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 contiene un int local, es muy probable que cambie el valor de a (o la dirección de memoria a la que solía apuntar). 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.


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 sólo 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 (proceso) (de manera segura, por ahora por lo menos). Después de que se devolvió la función 'foo', la dirección de 'a' puede considerarse 'sucia' pero está ahí, no se ha limpiado, ni se ha alterado / modificado con 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" (aunque puede advertirle, si le importa). Puede usar (actualizar) de forma segura cualquier ubicación de memoria que se encuentre en el segmento de datos de su instancia (proceso) del programa, a menos que proteja la dirección de alguna manera.


Ese es un comportamiento clásico indefinido que se ha discutido aquí 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 inválido a la memoria.


Funciona porque la pila no ha sido alterada (aún) desde que se colocó allí. Llame a algunas otras funciones (que también están llamando a otras funciones) antes de acceder a a nuevo y probablemente ya no tenga tanta suerte ... ;-)


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):

char* foo() 
{
  char buf[10];
  ::strcpy(buf, "TEST”);
  return buf;
}

int main() 
{
  char* s = foo();    //place breakpoint & check 's' varialbe here
  ::printf("%s\n", s); 
}

Nunca lanzas una excepción de C ++ accediendo a memoria inválida. Solo está dando un ejemplo de la idea general de hacer referencia a una ubicación de memoria arbitraria. Yo podría hacer lo mismo así:

unsigned int q = 123456;

*(double*)(q) = 1.2;

Aquí simplemente trato a 123456 como la dirección de un doble y le escribo. Pueden pasar muchas cosas:

  1. q podría, de hecho, ser realmente una dirección válida de doble, por ejemplo, double p; q = &p; double p; q = &p; .
  2. q podría apuntar a algún lugar dentro de la memoria asignada y simplemente sobrescribo 8 bytes allí.
  3. q puntos q 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 termine.
  4. Usted gana la lotería.

La forma en que lo configura es un poco más razonable que la dirección devuelta apunte a un área válida de la memoria, ya que probablemente solo 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 una La moda determinista.

Nadie verificará automáticamente la validez semántica de las direcciones de memoria de esa manera durante la ejecución normal del programa. Sin embargo, un depurador de memoria como valgrind hará felizmente, por lo que debe ejecutar su programa a través de él y ser testigo de los errores.


Porque el espacio de almacenamiento no estaba pisoteado todavía. No cuente con ese comportamiento.


Puede, porque a es una variable asignada temporalmente para la vida útil de su alcance (función foo ). Después de regresar de foo 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.


Solo está devolviendo una dirección de memoria, está permitido, pero probablemente un error.

Sí, si intenta eliminar la referencia a esa dirección de memoria, tendrá un comportamiento indefinido.

int * ref () {

 int tmp = 100;
 return &tmp;
}

int main () {

 int * a = ref();
 //Up until this point there is defined results
 //You can even print the address returned
 // but yes probably a bug

 cout << *a << endl;//Undefined results
}

Una pequeña adición a todas las respuestas:

si haces algo asi

#include<stdio.h>
#include <stdlib.h>
int * foo(){
    int a = 5;
    return &a;
}
void boo(){
    int a = 7;

}
int main(){
    int * p = foo();
    boo();
    printf("%d\n",*p);
}

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 desmontas el ejecutable, lo verás claramente.





dangling-pointer