¿Dónde se almacenan los valores nulos, o se almacenan en absoluto?

39

Quiero aprender sobre valores nulos o referencias nulas.

Por ejemplo, tengo una clase llamada Apple y creé una instancia de ella.

Apple myApple = new Apple("yummy"); // The data is stored in memory

Luego comí esa manzana y ahora tiene que ser nula, así que la puse como nula.

myApple = null;

Después de esta llamada, olvidé que me la comí y ahora quiero verificar.

bool isEaten = (myApple == null);

Con esta llamada, ¿dónde se hace referencia a myApple? ¿Es nulo un valor de puntero especial? Si es así, si tengo 1000 objetos nulos, ¿ocupan 1000 espacios de memoria de objetos o 1000 espacios de memoria int si pensamos que un tipo de puntero es int?

Mert Akcakaya
fuente

Respuestas:

45

En su ejemplo, myAppletiene el valor especial null(generalmente todos los bits cero), por lo que no hace referencia a nada. El objeto al que se refería originalmente ahora se pierde en el montón. No hay forma de recuperar su ubicación. Esto se conoce como pérdida de memoria en sistemas sin recolección de basura.

Si originalmente configuró 1000 referencias en nulo, entonces tiene espacio para solo 1000 referencias, generalmente 1000 * 4 bytes (en un sistema de 32 bits, el doble que en 64). Si esas 1000 referencias apuntaban originalmente a objetos reales, entonces asignó 1000 veces el tamaño de cada objeto, más espacio para las 1000 referencias.

En algunos lenguajes (como C y C ++), los punteros siempre apuntan a algo, incluso cuando están "sin inicializar". El problema es si la dirección que tienen es legal para que su programa acceda. La dirección especial cero (aka null) no se asigna deliberadamente a su espacio de direcciones, por lo que la unidad de administración de memoria (MMU) genera un error de segmentación cuando se accede a él y su programa se bloquea. Pero dado que la dirección cero deliberadamente no está asignada, se convierte en un valor ideal para usar para indicar que un puntero no apunta a nada, de ahí su papel como null. Para completar la historia, al asignar memoria con newomalloc(), el sistema operativo configura la MMU para asignar páginas de RAM en su espacio de direcciones y se vuelven utilizables. Por lo general, todavía hay vastos rangos de espacio de direcciones que no están asignados y, por lo tanto, también provocan fallas de segmentación.

Randall Cook
fuente
Muy buena explicación.
NoPuerto
66
Está un poco mal en la parte de "pérdida de memoria". Es una pérdida de memoria en sistemas sin administración automática de memoria. Sin embargo, GC no es la única forma posible de implementar la administración automática de memoria. C ++ std::shared_ptr<Apple>es un ejemplo que no es ni GC ni pierde Applecuando se pone a cero.
MSalters
1
@MSalters: ¿no es shared_ptrsolo una forma básica de recolección de basura? GC no requiere que haya un "recolector de basura" separado, solo que se produce la recolección de basura.
Restablece a Mónica
55
@Brendan: El término "recolección de basura" se entiende casi universalmente para referirse a la recolección no determinista que tiene lugar independientemente de la ruta de código normal. La destrucción determinista basada en el recuento de referencias es algo completamente diferente.
Mason Wheeler
1
Buena explicación. Un punto ligeramente engañoso es la suposición de que la asignación de memoria se asigna a la RAM. La RAM es un mecanismo para el almacenamiento de memoria a corto plazo, pero el sistema operativo abstrae el mecanismo de almacenamiento real. En Windows (para aplicaciones que no son de anillo cero), las páginas de memoria están virtualizadas y pueden correlacionarse con RAM, archivo de intercambio de disco u otro dispositivo de almacenamiento.
Simon Gillbee
13

La respuesta depende del idioma que estés usando.

C / C ++

En C y C ++, la palabra clave era NULL, y lo que realmente era NULL era 0. Se decidió que "0x0000" nunca sería un puntero válido a un objeto, y ese es el valor que se asigna para indicar que No es un puntero válido. Sin embargo, es completamente arbitrario. Si intentas acceder a él como un puntero, se comportaría exactamente como un puntero a un objeto que ya no existe en la memoria, provocando una excepción de puntero no válida. El puntero mismo ocupa memoria, pero no más de lo que lo haría un objeto entero. Por lo tanto, si tiene 1000 punteros nulos, es el equivalente a 1000 enteros. Si algunos de esos punteros apuntan a objetos válidos, entonces el uso de la memoria sería el equivalente a 1000 enteros más la memoria contenida en esos punteros válidos. Recuerda que en C o C ++,no implica que se haya liberado memoria, por lo que debe eliminar explícitamente ese objeto utilizando dealloc (C) o eliminar (C ++).

Java

A diferencia de C y C ++, en Java nulo es simplemente una palabra clave. En lugar de administrar nulo como un puntero a un objeto, se administra internamente y se trata como un literal. Esto eliminó la necesidad de vincular los punteros como tipos enteros y permite que Java abstraiga los punteros por completo. Sin embargo, incluso si Java lo oculta mejor, siguen siendo punteros, lo que significa que 1000 punteros nulos aún consumen el equivalente de 1000 enteros. Obviamente, cuando apuntan a objetos, al igual que C y C ++, la memoria es consumida por esos objetos hasta que no haya más punteros que los hagan referencia, sin embargo, a diferencia de C y C ++, el recolector de basura lo recoge en su próxima pasada y libera la memoria, sin requerir que tenga que hacer un seguimiento de qué objetos se liberan y cuáles no, en la mayoría de los casos (a menos que tenga razones para hacer referencia débil a los objetos, por ejemplo).

Neil
fuente
9
Su distinción no es correcta: de hecho, en C y C ++, el puntero nulo no necesita apuntar a la dirección de memoria 0 (aunque esta es la implementación natural, igual que en Java y C #). Puede apuntar literalmente a cualquier parte. Esto se confunde ligeramente por el hecho de que literal-0 se puede convertir implícitamente en un puntero nulo. Pero el patrón de bits almacenado para un puntero nulo aún no necesita ser todos ceros.
Konrad Rudolph
1
No, estás equivocado. La semántica es completamente transparente ... en el programa, los punteros nulos y la macro NULL( por cierto, no una palabra clave) se tratan como si fueran cero bits. Pero no necesitan implementarse como tales, y de hecho algunas implementaciones oscuras usan punteros nulos distintos de cero. Si escribo if (myptr == 0), el compilador hará lo correcto, incluso si el puntero nulo está representado internamente por 0xabcdef.
Konrad Rudolph
3
@Neil: una constante de puntero nulo (valor de tipo entero que se evalúa a cero) es convertible a un valor de puntero nulo . (§4.10 C ++ 11.) No se garantiza que un valor de puntero nulo tenga todos los bits cero. 0es una constante de puntero nulo, pero esto no significa que myptr == 0compruebe si todos los bits de myptrson cero.
Mat
55
@Neil: es posible que desee comprobar esta entrada en el faq C o esta pregunta SO
hugomg
1
@Neil Es por eso que me esforcé por no mencionar la NULLmacro en absoluto, sino por hablar sobre el "puntero nulo" y mencionar explícitamente que "literal-0 se puede convertir implícitamente en un puntero nulo".
Konrad Rudolph,
5

Un puntero es simplemente una variable que es principalmente de tipo entero. Especifica una dirección de memoria donde se almacena el objeto real.

La mayoría de los idiomas permiten acceder a los miembros del objeto a través de esta variable de puntero:

int localInt = myApple.appleInt;

El compilador sabe cómo acceder a los miembros de un Apple. "Sigue" el puntero a myApplela dirección y recupera el valor deappleInt

Si asigna el puntero nulo a una variable de puntero, hace que el puntero apunte a ninguna dirección de memoria. (Lo que hace imposible el acceso de los miembros).

Para cada puntero necesita memoria para mantener el valor entero de la dirección de memoria (principalmente 4 bytes en sistemas de 32 bits, 8 bytes en sistemas de 64 bits). Esto también es cierto para los punteros nulos.

Stephan
fuente
Creo que las variables / objetos de referencia no son exactamente punteros. Si los imprime, contienen ClassName @ Hashcode. JVM usa Hashtable internamente para almacenar Hashcode con la dirección real y utiliza un Algoritmo Hash para recuperar la dirección real cuando sea necesario.
menosSeven
@minusSeven Eso es correcto para lo que concierne a objetos literales como enteros. De lo contrario, la tabla hash contiene punteros a otros objetos contenidos dentro de la clase Apple.
Neil
@minusSeven: estoy de acuerdo. Los detalles de la implementación del puntero dependen en gran medida del idioma / tiempo de ejecución. Pero creo que esos detalles no son tan relevantes para la pregunta específica.
Stephan
4

Ejemplo rápido (tenga en cuenta que los nombres de variables no se almacenan):

void main()
{
  int X = 3;
  int *Y = X;
  int *Z = null;
} // void main(...)


...........................
....+-----+--------+.......
....|     |   X    |.......
....+-----+--------+.......
....| 100 |   3    |<---+..
....+-----+--------+....|..
........................|..
....+-----+--------+....|..
....|     |   Y    |....|..
....+-----+--------+....|..
....| 102 |  100   +----+..
....+-----+--------+.......
...........................
....+-----+--------+.......
....|     |   z    |.......
....+-----+--------+.......
....| 104 |   0    |.......
....+-----+--------+.......
...........................

Aclamaciones.

umlcat
fuente