Los punteros son un concepto que para muchos puede ser confuso al principio, en particular cuando se trata de copiar valores de puntero y aún hacer referencia al mismo bloque de memoria.
He descubierto que la mejor analogía es considerar el puntero como un trozo de papel con una dirección de la casa y el bloque de memoria al que hace referencia como la casa real. Todo tipo de operaciones se pueden explicar fácilmente.
He agregado un código de Delphi a continuación, y algunos comentarios cuando corresponde. Elegí Delphi porque mi otro lenguaje de programación principal, C #, no exhibe cosas como pérdidas de memoria de la misma manera.
Si solo desea aprender el concepto de alto nivel de punteros, debe ignorar las partes etiquetadas como "Diseño de memoria" en la explicación a continuación. Su objetivo es dar ejemplos de cómo se vería la memoria después de las operaciones, pero son de naturaleza de nivel más bajo. Sin embargo, para explicar con precisión cómo funcionan realmente los desbordamientos del búfer, fue importante que agregue estos diagramas.
Descargo de responsabilidad: para todos los efectos, esta explicación y los diseños de memoria de ejemplo se simplifican enormemente. Hay más gastos generales y muchos más detalles que necesitaría saber si necesita lidiar con la memoria a bajo nivel. Sin embargo, para los intentos de explicar la memoria y los punteros, es lo suficientemente preciso.
Supongamos que la clase THouse utilizada a continuación se ve así:
type
THouse = class
private
FName : array[0..9] of Char;
public
constructor Create(name: PChar);
end;
Cuando inicializa el objeto de la casa, el nombre dado al constructor se copia en el campo privado FName. Hay una razón por la que se define como una matriz de tamaño fijo.
En la memoria, habrá algunos gastos generales asociados con la asignación de la casa, ilustraré esto a continuación de esta manera:
--- [ttttNNNNNNNNNN] ---
^ ^
El | El |
El | + - la matriz FName
El |
+ - gastos generales
El área "tttt" está sobrecargada, normalmente habrá más de esto para varios tipos de tiempos de ejecución e idiomas, como 8 o 12 bytes. Es imperativo que cualquier valor que se almacene en esta área nunca se modifique por otra cosa que no sea el asignador de memoria o las rutinas centrales del sistema, o corre el riesgo de bloquear el programa.
Asignar memoria
Haz que un emprendedor construya tu casa y te dé la dirección de la casa. A diferencia del mundo real, no se puede decir a la asignación de memoria dónde asignar, pero encontrará un lugar adecuado con suficiente espacio e informará la dirección a la memoria asignada.
En otras palabras, el empresario elegirá el lugar.
THouse.Create('My house');
Diseño de memoria:
--- [ttttNNNNNNNNNN] ---
1234Mi casa
Mantenga una variable con la dirección
Escriba la dirección de su nueva casa en una hoja de papel. Este documento servirá como referencia para su casa. Sin este pedazo de papel, estás perdido y no puedes encontrar la casa, a menos que ya estés en ella.
var
h: THouse;
begin
h := THouse.Create('My house');
...
Diseño de memoria:
h
v
--- [ttttNNNNNNNNNN] ---
1234Mi casa
Copiar valor del puntero
Simplemente escriba la dirección en una nueva hoja de papel. Ahora tiene dos hojas de papel que lo llevarán a la misma casa, no a dos casas separadas. Cualquier intento de seguir la dirección de un documento y reorganizar los muebles en esa casa hará que parezca que la otra casa ha sido modificada de la misma manera, a menos que pueda detectar explícitamente que en realidad es solo una casa.
Nota: Este es generalmente el concepto que tengo más problemas para explicar a las personas, dos punteros no significa dos objetos o bloques de memoria.
var
h1, h2: THouse;
begin
h1 := THouse.Create('My house');
h2 := h1; // copies the address, not the house
...
h1
v
--- [ttttNNNNNNNNNN] ---
1234Mi casa
^
h2
Liberando la memoria
Demoler la casa. Luego, puede reutilizar el papel para una nueva dirección si así lo desea, o borrarlo para olvidar la dirección de la casa que ya no existe.
var
h: THouse;
begin
h := THouse.Create('My house');
...
h.Free;
h := nil;
Aquí primero construyo la casa y obtengo su dirección. Luego hago algo a la casa (lo uso, el ... código, lo dejé como ejercicio para el lector), y luego lo libero. Por último, borro la dirección de mi variable.
Diseño de memoria:
h <- +
v + - antes gratis
--- [ttttNNNNNNNNNN] --- |
1234Mi casa <- +
h (ahora apunta a ninguna parte) <- +
+ - después de gratis
---------------------- | (nota, la memoria aún podría
xx34 Mi casa <- + contiene algunos datos)
Punteros colgantes
Le dice a su empresario que destruya la casa, pero se olvida de borrar la dirección de su hoja de papel. Cuando más tarde mira el pedazo de papel, ha olvidado que la casa ya no está allí y va a visitarlo, con resultados fallidos (vea también la parte sobre una referencia no válida a continuación).
var
h: THouse;
begin
h := THouse.Create('My house');
...
h.Free;
... // forgot to clear h here
h.OpenFrontDoor; // will most likely fail
Usar h
después de la llamada a .Free
podría funcionar, pero eso es pura suerte. Lo más probable es que falle, en el lugar de un cliente, en medio de una operación crítica.
h <- +
v + - antes gratis
--- [ttttNNNNNNNNNN] --- |
1234Mi casa <- +
h <- +
v + - después de liberar
---------------------- |
xx34Mi casa <- +
Como puede ver, h todavía apunta a los remanentes de los datos en la memoria, pero dado que puede no estar completo, su uso como antes podría fallar.
Pérdida de memoria
Pierdes el papel y no puedes encontrar la casa. Sin embargo, la casa todavía está parada en algún lugar, y cuando más tarde quieras construir una nueva casa, no puedes reutilizar ese lugar.
var
h: THouse;
begin
h := THouse.Create('My house');
h := THouse.Create('My house'); // uh-oh, what happened to our first house?
...
h.Free;
h := nil;
Aquí sobrescribimos el contenido de la h
variable con la dirección de una casa nueva, pero la antigua todavía está en pie ... en algún lugar. Después de este código, no hay forma de llegar a esa casa, y se dejará en pie. En otras palabras, la memoria asignada permanecerá asignada hasta que se cierre la aplicación, momento en el cual el sistema operativo la destruirá.
Diseño de memoria después de la primera asignación:
h
v
--- [ttttNNNNNNNNNN] ---
1234Mi casa
Diseño de memoria después de la segunda asignación:
h
v
--- [ttttNNNNNNNNNN] --- [ttttNNNNNNNNNN]
1234Mi casa 5678Mi casa
Una forma más común de obtener este método es olvidarse de liberar algo, en lugar de sobrescribirlo como se indicó anteriormente. En términos de Delphi, esto ocurrirá con el siguiente método:
procedure OpenTheFrontDoorOfANewHouse;
var
h: THouse;
begin
h := THouse.Create('My house');
h.OpenFrontDoor;
// uh-oh, no .Free here, where does the address go?
end;
Después de que este método se haya ejecutado, no hay lugar en nuestras variables donde exista la dirección de la casa, pero la casa todavía está ahí afuera.
Diseño de memoria:
h <- +
v + - antes de perder el puntero
--- [ttttNNNNNNNNNN] --- |
1234Mi casa <- +
h (ahora apunta a ninguna parte) <- +
+ - después de perder el puntero
--- [ttttNNNNNNNNNN] --- |
1234Mi casa <- +
Como puede ver, los datos antiguos se dejan intactos en la memoria y el asignador de memoria no los reutilizará. El asignador realiza un seguimiento de las áreas de memoria que se han utilizado y no las reutilizará a menos que lo libere.
Liberando la memoria pero manteniendo una referencia (ahora no válida)
Demoler la casa, borrar una de las hojas de papel, pero también tiene otra hoja de papel con la dirección anterior, cuando vaya a la dirección, no encontrará una casa, pero puede encontrar algo que se parezca a las ruinas de uno.
Tal vez incluso encuentre una casa, pero no es la casa a la que se le dio la dirección originalmente, y por lo tanto, cualquier intento de usarla como si le perteneciera podría fallar horriblemente.
A veces, incluso puede encontrar que una dirección vecina tiene una casa bastante grande que ocupa tres direcciones (Main Street 1-3), y su dirección va al centro de la casa. Cualquier intento de tratar esa parte de la gran casa de 3 direcciones como una sola casa pequeña también podría fallar horriblemente.
var
h1, h2: THouse;
begin
h1 := THouse.Create('My house');
h2 := h1; // copies the address, not the house
...
h1.Free;
h1 := nil;
h2.OpenFrontDoor; // uh-oh, what happened to our house?
Aquí la casa fue demolida, a través de la referencia h1
, y aunque también h1
fue limpiada, h2
todavía tiene la dirección antigua y desactualizada. El acceso a la casa que ya no está en pie podría o no funcionar.
Esta es una variación del puntero colgante anterior. Ver su diseño de memoria.
Tampón desbordado
Mueves más cosas a la casa de las que puedes caber, derramando en la casa o patio de los vecinos. Cuando el dueño de esa casa vecina más tarde vuelva a casa, encontrará todo tipo de cosas que considerará suyas.
Esta es la razón por la que elegí una matriz de tamaño fijo. Para establecer el escenario, suponga que la segunda casa que asignamos, por alguna razón, se colocará antes que la primera en la memoria. En otras palabras, la segunda casa tendrá una dirección más baja que la primera. Además, están asignados uno al lado del otro.
Por lo tanto, este código:
var
h1, h2: THouse;
begin
h1 := THouse.Create('My house');
h2 := THouse.Create('My other house somewhere');
^-----------------------^
longer than 10 characters
0123456789 <-- 10 characters
Diseño de memoria después de la primera asignación:
h1
v
----------------------- [ttttNNNNNNNNNN]
5678Mi casa
Diseño de memoria después de la segunda asignación:
h2 h1
vv
--- [ttttNNNNNNNNNN] ---- [ttttNNNNNNNNNN]
1234 Mi otra casa en algún lugar
^ --- + - ^
El |
+ - sobrescrito
La parte que con mayor frecuencia provocará un bloqueo es cuando sobrescribe partes importantes de los datos que almacenó que realmente no deberían modificarse al azar. Por ejemplo, podría no ser un problema que se cambiaran partes del nombre de la casa h1, en términos de bloquear el programa, pero sobreescribir la sobrecarga del objeto probablemente se bloqueará cuando intente usar el objeto roto, como lo hará sobrescribir enlaces que se almacenan en otros objetos en el objeto.
Listas enlazadas
Cuando sigue una dirección en una hoja de papel, llega a una casa, y en esa casa hay otra hoja de papel con una nueva dirección, para la siguiente casa en la cadena, y así sucesivamente.
var
h1, h2: THouse;
begin
h1 := THouse.Create('Home');
h2 := THouse.Create('Cabin');
h1.NextHouse := h2;
Aquí creamos un enlace desde nuestra casa hasta nuestra cabaña. Podemos seguir la cadena hasta que una casa no tenga NextHouse
referencia, lo que significa que es la última. Para visitar todas nuestras casas, podríamos usar el siguiente código:
var
h1, h2: THouse;
h: THouse;
begin
h1 := THouse.Create('Home');
h2 := THouse.Create('Cabin');
h1.NextHouse := h2;
...
h := h1;
while h <> nil do
begin
h.LockAllDoors;
h.CloseAllWindows;
h := h.NextHouse;
end;
Diseño de memoria (se agregó NextHouse como un enlace en el objeto, indicado con los cuatro LLLL en el siguiente diagrama):
h1 h2
vv
--- [ttttNNNNNNNNNNLLLL] ---- [ttttNNNNNNNNNNLLLL]
1234 Casa + 5678 Cabina +
El | ^ |
+ -------- + * (sin enlace)
En términos básicos, ¿qué es una dirección de memoria?
Una dirección de memoria es, en términos básicos, solo un número. Si piensa en la memoria como una gran variedad de bytes, el primer byte tiene la dirección 0, el siguiente la dirección 1 y así sucesivamente. Esto es simplificado, pero lo suficientemente bueno.
Entonces este diseño de memoria:
h1 h2
vv
--- [ttttNNNNNNNNNN] --- [ttttNNNNNNNNNN]
1234Mi casa 5678Mi casa
Puede tener estas dos direcciones (la más a la izquierda - es la dirección 0):
Lo que significa que nuestra lista de enlaces anterior podría verse así:
h1 (= 4) h2 (= 28)
vv
--- [ttttNNNNNNNNNNLLLL] ---- [ttttNNNNNNNNNNLLLL]
1234 Inicio 0028 5678 Cabina 0000
El | ^ |
+ -------- + * (sin enlace)
Es típico almacenar una dirección que "apunta a ninguna parte" como una dirección cero.
En términos básicos, ¿qué es un puntero?
Un puntero es solo una variable que contiene una dirección de memoria. Por lo general, puede pedirle al lenguaje de programación que le dé su número, pero la mayoría de los lenguajes de programación y tiempos de ejecución intenta ocultar el hecho de que hay un número debajo, solo porque el número en sí no tiene ningún significado para usted. Es mejor pensar en un puntero como un cuadro negro, es decir. usted realmente no sabe ni le importa cómo se implementa realmente, siempre y cuando funcione.