LOS PUNTEROS CAPITULO 8
Lo primero: hay que entender muy bien lo que es un
puntero, porque al principio puede se un poco complicado. Un puntero es un tipo
de datos (como un INTEGER, por ejemplo) que lo que almacena no es ni un número
ni una letra sino una dirección de memoria. Una posición de memoria es una
"cosa" del tipo 0xxxxh:0xxxxh que identifica una posición de la
memoria
CONVENCIONAL del ordenador. (Recordemos que el TP 7 sólo
sabe hacer programas para 286, o sea: para modo real, por lo tanto sólo podemos
usar la memoria convencional [esto en realidad es falso, se puede usar memoria
XMS y EMS desde Pascal, pero eso ya es otro tema])
Se dice que un puntero APUNTA a la dirección de memoria.
Para declararlo tenemos el tipo POINTER, con lo que lo
declaramos igual que cualquier otro tipo de datos:
Var
Puntero: Pointer;
¿Y esto para qué nos sirve? Pues así, con lo que sabemos
ahora, de nada. Pero lo curioso de los punteros es que podemos decirle qué es
lo que hay en la posición de memoria a la que está apuntando.
Me explico: el puntero "señala" a un lugar de
la memoria de nuestro PC. Pues ese lugar podemos tratarlo como un BYTE, o como
un WORD, como un STRING, como una estructura de datos o incluso como otro
puntero!
En este caso la declaración cambia. En este caso se
declara igual que si declarásemos una variable de un tipo concreto sólo que
precedemos el nombre del tipo por el símbolo ^. (ALT+94)
Así para declarar un puntero de manera que la memoria a
la que apunta sea tratada como un byte haríamos esto:
Var
Puntero: ^Byte;
Y lo mismo para una estructura:
Type
Tipo =
record
Nombre :
String;
DNI :
Longint;
NIF : Char;
Calle :
String;
Edad : Word;
End;
Var
Puntero : ^Tipo;
Este tipo de punteros se denominan de una manera
especial, son punteros A el tipo de datos. Es decir, este último ejemplo sería
un puntero A una estructura (o a la estructura Tipo), el anterior sería un
puntero a un byte, etc... Y también pueden hacerse punteros a punteros,
punteros a arrays de punteros, etc... pero eso es más complicado y vendrá más
adelante.
Pero... ¿a dónde apunta un puntero? Eeeeiiinngg? ;) Me
refiero: un puntero, nada más empezar el programa ¿apunta a alguna parte? ¿a
dónde apunta? Pues lo más normal es que apunte a la dirección 0000:0000, ya que
las variables suelen inicializarse a cero. Por lo tanto antes de usar el
puntero tenemos que hacer que apunte al sitio que nosotros queremos. ¿Qué pasa
si no lo hacemos? Pues si modificamos un puntero que apunta a 0000:0000... en
esa zona de la memoria está la tabla de los vectores de interrupción, si lo
modificamos nos cepillaremos esa tabla, con lo cual, en cuanto se genere una
interrupción del temporizador (una cada 55 milisegundos) nuestro programa nos
brindará un precioso cuelgue.
Este tipo de punteros puede ser más útil, porque... Ya
que un puntero puede apuntar a cualquier parte de la memoria convencional...
podemos modificar cualquier posición de memoria, tanto si pertenece a nuestro
programa como si no. Por ejemplo, el WORD de la posición 040h:02h contiene la
direción del puerto base para el COM2, para leerla sólo tenemos que hacer que
un puntero a un word apunte a esa direción (040h:02h) y después leer lo que hay
allí. Para hacer que un puntero apunte a un sitio determinado tenemos la
función Ptr. La función Ptr acepta como parámetros el segmento y el offset de
la direción de memoria y nos devuelve como resultado un puntero a esa direción.
Así, es programa que lee la direción del COM2 quedaría así:
Var
Puntero_a_COM2 : ^Word;
Begin
Puntero_a_COM2 := Ptr($40,2);
WriteLn('La direción del COM2 es ',Puntero_a_COM2^);
End.
Una cosa importante a tener en cuenta es que no es lo
mismo la direción de memoria que lo que hay en esa direción. El puntero sólo
contiene la direción, no contiene lo que hay en ella. Es decir: el puntero sólo
señala al WORD donde está la direción de COM2. El puntero NO almacena la
posición del COM2. Por eso estas dos lieas son bien diferentes:
Puntero := 8;
Puntero^ := 8;
La primera intenta asignar el valor 8 como direción de
memoria a la cual tiene que apuntar el puntero. Mientras que la segunda lo que
hace es asignar el valor 8 a la direción de memoria a la que apunta el puntero.
Es muy importante que esto quede claro.
Podríamos decir que Puntero^ es el elemento y Puntero es
la direción del elemento.
Pero la utilidad de los punteros no acaba aquí ni mucho
menos.
Creo que ya antes había dicho que las variables de un
programa sólo podían ocupar 64 Kbs como mucho. Y... ¿qué ha pasado con en resto
de los 640 Kbs de memoria? Pues esa memoria se puede usar mediante punteros.
Para pedir memoria tenemos la función Getmem y para
liberarla una vez ya no la necesitamos tenemos Freemem. A ambas hay que
pasarles un puntero y el tamaño de memoria que queremos reservar. Por ejemplo:
Getmem(Puntero, 30000);
Esto lo que haría sería reservar 30000 bytes de memoria y
hacer que el puntero apunte al inicio de esos 30000 bytes. ¿A qué tipo de datos
tiene que apuntar este puntero? A cualquiera. Si hemos declarado Puntero como
un puntero a un byte podremos usar
"Puntero^ := valor;" para modificar el primer
byte del bloque de memoria que hemos pedido.
¿Y qué pasa con el resto? Pues muy sencillo. Podemos
"mover" el puntero para que apunte al segundo byte del bloque, o al
tercero... Para eso tenemos las funciones INC y DEC. Con INC(PUNTERO); hacemos
apuntar el puntero a el siguiente elemento.
PERO OJO!!! El siguiente elemento no siempre será el
siguiente byte. Si hemos definido el puntero como un puntero a words cada vez
que usemos INC avanzaremos 2 bytes (que es lo que ocupa un
WORD). Y DEC se usa igual y tiene la función inversa.
La pega que tiene este método es que nadie nos impide
hacer 600.000 INCs y poner el puntero apuntando a una parte de la memoria que
no hemos reservado. Y es que para bloques de memoria hay maneras más prácticas de
manejarlos.
Para esto son muy útiles los punteros a arrays. Podemos
definir un así:
Type
P = Array [0..29999] of Byte;
Var
Puntero : ^P;
Begin
Getmem(Puntero, 30000);
End.
De esta manera definimos un array que empieza en la
direción a la que apunta el puntero. Esta direción, después del Getmem, será la
direción del bloque de memoria. Y a partir de esa direción tenemos 29999
elementos más en el array. Cada elemento es un Byte, por lo tanto, a cada
posición del array le corresponde un byte del bloque de memoria que hemos
reservado.
Ahora, para leer/escribir en ese bloque sólo tenemos que
hacer cosas como esta:
Puntero^[8] := Puntero^[15003] - 80;
Fácil ¿no? Pero... ¿qué pasa si queremos poner todo el
bloque a cero? Podríamos hacer un bucle FOR que fuese poniendo las posiciones a
cero una por una, pero hay una manera mejor. Tenemos la instrución FillChar.
Esta instrución llena con un valor dado el número de posiciones que le digamos
a partir de la posición que le demos. Se usaría así:
FillChar(Puntero^, 30000, 0);
Atención a que ponemos "Puntero^" y no
"Puntero". ¿Por qué? Pues porque lo que queremos llenar de ceros no
es el puntero sino la memoria a la que apunta ese puntero. (Ya vereis que
cuelgues más majos os salen cuando se os olvide esto ;)
Y... ¿qué pasa si tenemos 2 bloques de memoria y queremos
copiar todo lo que haya en uno al otro? Pues podríamos hacer un FOR, pero hay
una función específica para eso: Move. Move se usaría así:
Move(Puntero_Fuente^, Puntero_Destino^, Tamaño);
Cuando ya hayamos hecho todo lo que queríamos con un
bloque de memoria debemos liberarlo. Para eso usamos Freemem igual que si fuese
GetMem: Freemem(Puntero, Tamaño). Hay que tener en cuenta que el puntero debe
apuntar exactamente al mismo sitio que despuésde hacer el GetMem. Es decir: si
hemos usado algún INC o DEC con él debemos asegurarnos que lo dejamos tal y
como estaba antes de llamar a Freemem.
Ahora supongamos que tenemos un puntero que apunta a una
estructura y queremos reservar la memoria justa para que quepa esa estructura.
Podemos contar los bytes que ocupa esa estructura (no estaría mal para
practicar ;) o podemos usar New en lugar de Getmem. A New sólo hay que pasarle
el puntero: New(Puntero);
Y el equivalente a Freemem entonces será Dispose, que usaremos
igual que New.
Y ahora el punto más interesante. Antes de empezarlo os
presento a NIL. NIL es una constante, pero no es una constante numérica sino un
puntero. Y ¿a dónde apunta este puntero? En la ayuda dice que no apunta a
ninguna parte. Y... aunque un puntero SIEMPRE apunta a alguna parte haremos
como si fuese verdad que no apunta a nada.
Así podemos decir que NIL es el puntero nulo. Ahora
empecemos: declaramos un puntero así:
Type
PEsctructura = ^Estructura;
Estructura = record
Nombre : String;
Siguiente : PEstructura;
End;
Var
Puntero : PEstructura;
Bien... y reservamos memoria para el puntero:
New(Puntero);
En este momento ya tenemos un puntero apuntando a una
estructura que consta de una cadena de caracteres y de otro puntero.
Rellenamos el primer campo:
Puntero^.Nombre := 'Pepito';
Y pedimos memoria para el segundo (ya que un puntero no
podemos usarlo si no hemos pedido memoria para él o le hemos hecho apuntar a
alguna parte).
New(Puntero^.Siguiente);
Ahora el puntero Siguiente apunta a una estructura de
datos que consta de 2 campos: una cadena de caracteres y un puntero. ¿os suena
de algo? ;)
Podemos volver a pedir memoria para el puntero de esta
segunda estructura y luego pedir memoria para el puntero de la tercera, etc,
etc...
¿Y qué ventaja tiene esto? Pues que podemos irlo
repitiendo todas las veces que queramos hasta que se nos acabe la memoria. En
un array hay que definir el número de elementos a la hora de escribir el
programa, pero de esta manera podemos ir llenanado más y más elementos sin
tener un límite prefijado.
La pega ahora es... ¿Cómo se trabaja con esto? Pues bien,
para dejarlo lo más claro posible os pongo un ejemplo sencillo:
Type
P2 =
^Estruc;
Estruc =
record
Cadena :
String;
Siguiente : P2;
End;
Var
Puntero, PAux : P2;
Cadena :
String;
Begin
WriteLn('Memoria
libre: ',maxavail);
WriteLn('Escribe nombres. (una linea en blanco para
terminar)');
New(Puntero);
PAux := Puntero;
PAux^.Siguiente := Nil;
Repeat
ReadLn(Cadena);
PAux^.Cadena := Cadena;
if Cadena
<> '' Then
Begin
New(PAux^.Siguiente);
PAux := PAux^.Siguiente;
PAux^.Siguiente := Nil;
End;
Until Cadena = '';
WriteLn;
WriteLn('Has
escrito:');
PAux := Puntero;
While PAux^.Siguiente <> Nil do
Begin
WriteLn(Paux^.Cadena);
Puntero := Paux;
PAux := Paux^.Siguiente;
Dispose(Puntero);
End;
Dispose(Paux);
WriteLn('Memoria
libre: ',maxavail);
End.
Y esta ha sido la última entrega del curso, espero que les aya servido, pueden seguir con el siguiente curso que es Metodología de la Programación Pascal
0 comentarios: