|
|---|
Variables y Punteros
VARIABLES
Supongamos que la memoria de una máquina de éstas es lineal (en realidad, lo es para cualquier aplicación
que trabaje en Windows). Es decir, las celdas de memoria de los SIMM de memoria se sitúan una tras otra.
En una máquina 386 la unidad de memoria más pequeña que puede ser "direccionada"
(ahora voy con esto) es el BYTE. Con "direccionar" se entiende el acceso a una celda de memoria, es decir,
a poder conocer y/o cambiar lo que tenga la celda de memoria.
Una representación esquemática de la memoria podría ser la siguiente:
F E D C B A 9 8 7 6 5 4 3 2 1 0
| x |
donde la 'x' de la celda 7 sería el lugar en que el compilador ha situado a la variable 'Mi_Byte'.
Ahora bien, ¿qué valor tiene 'Mi_Byte'? Nuevamente: ni idea. En programación se suele denominar
"basura" al valor que toma una VARIABLE NO INICIALIZADA. Ahora mismo explico qué significa eso.
La memoria física de la máquina se va utilizando y liberando. Así, podemos arrancar el Explorador
de Windows (que ocupará varias celdas de memoria) o la calculadora (que ocupará otras más)
y, luego, descargar esas aplicaciones. Las celdas que ocupaban en memoria no "se limpian" durante la
descarga (en algunos sistemas, como Windows NT, 2000 o XP es posible que sí, pero ese es otro tema que tiene
que ver con la seguridad). Es decir que si, por ejemplo, tengo una aplicación en memoria y, luego, la descargo,
la memoria podría tener un aspecto así, tras la descarga:
F E D C B A 9 8 7 6 5 4 3 2 1 0
| 4 | 6 | 1 | 0 | 3 | 5 | 9 |
procedure TForm1.Button1Click(Sender: TObject);
var
Mi_Byte: Byte;
begin
if (0 < Mi_Byte) then // <-- Aquí se produce el "warning"
begin
// Hacer algo...
end;
end;
Es posible que, llegado al punto donde se produce el aviso, la variable 'Mi_Byte' valga más que cero o no.
Hasta no llegar a ese punto no se sabrá. Por ejemplo, si en vez de haber situado a 'Mi_Byte' en la celda
7, se hubiese puesto a la misma en la celda 8, habría tomado el valor cero (ver el esquema de memoria que
he puesto antes).
La variable 'Mi_Byte' del ejemplo anterior se crea al entrar en el procedimiento y se destruye al salir del mismo.
Dicho de otro modo, cada vez que se ejecuta el procedimiento anterior 'Mi_Byte' puede estar en un lugar distinto
de la memoria. Así que, si ejecutamos un número de veces suficiente el procedimiento es posible que
tome un valor mayor que cero. O no.
Al tiempo que una variable está disponible, es decir, al tiempo que va desde su creación a su destrucción
se le denomina DURACIÓN. Así, la duración de la variable 'Mi_Byte' del ejemplo anterior es
la que tarde en ejecutarse el procedimiento 'Button1Click()'.
La primera vez que se le da un valor a una variable, la variable se da por inicializada. Dicho en otros términos,
el proceso de INICIALIZACIÓN DE UNA VARIABLE es aquel por el que la variable toma un valor válido.
Así, para evitar el aviso ("warning") anterior, haríamos algo así:
procedure TForm1.Button1Click(Sender: TObject);
var
Mi_Byte: Byte; // <-- DECLARACIÓN DE LA VARIABLE <-------|
begin |
Mi_Byte := 8; // <-- INICIALIZACIÓN DE LA VARIABLE |
if (0 < Mi_Byte) then // <-- USO DE LA VARIABLE |
begin |
// Hacer algo... DURACIÓN
end; |
end; <--------------------------------------------------------|
Como se ve, para evitar el aviso ("warning"), antes de poder utilizar la variable hay que inicializarla,
dándole un valor, el que sea.
Los diferentes tipos de variables que podemos utilizar en un programa: Byte, Integer, Cardinal, Word, Single, Double,
Currency,... se denominan TIPOS DE DATOS PREDEFINIDOS o, sencillamente, TIPOS PREDEFINIDOS. Es decir, son tipos
que "conoce" el compilador.
Su conocimiento es tal que sabe el tamaño (el número de celdas) que debe reservar cuando declaramos
una variable de esos tipos. Así, sabe que para un Byte, debe reservar una celda y que para un Integer debe
reservar 4 celdas (4 bytes).
Además de los tipos predefinidos, podemos crear lo que se denomina TIPOS DEFINIDOS POR EL USUARIO, mediante
'record' o 'class'. También se sabe el tamaño de estos tipos ya que, antes de ser utilizados, deben
ser declarados.
Tanto para los tipos definidos por el usuario como para los predefinidos, se puede utilizar el operador 'SizeOf()'
para obtener
el tamaño (el número de celdas o bytes) que ocupa el tipo en memoria. Hay un caso especial, el de
las clases ('class') en el que 'SizeOf()' devuelve 4 (bytes), cosa realmente extraña.
Cuando en una aplicación declaramos una variable de un tipo predefinido lo que estamos haciendo, en realidad,
es declarar un sinónimo para referirnos a un conjunto de celdas de la memoria. Supongo que se entenderá
que sería muy difícil para un humano (se supone que un programador es un humano, algo muy discutible
en ciertos ambientes) estar continuamente refiriéndose a la celda 7 o a la $AB7689D0 (otra celda, esta vez
más "alejada" y en notación hexadecimal).
Así que, en vez de estar todo el rato haciendo referencia a la celda 7, utilizamos el sinónimo (la
variable) 'Mi_Byte'. De paso, nos olvidamos si 'Mi_Byte' está en la celda 7 o en cualquier otra ya que lo
que realmente nos interesa no es la celda de memoria sino el valor que tenga.
Lo que quiero que se entienda es que, al hacer
Mi_Byte := 8; // <-- INICIALIZACIÓN DE LA VARIABLE
estoy poniendo en la celda donde esté 'Mi_Byte' el valor 8:
ANTES DE LA INICIALIZACIÓN
F E D C B A 9 8 7 6 5 4 3 2 1 0
| 4 | 6 | 1 | 0 | 3 | 5 | 9 |
| 4 | 6 | 1 | 0 | 8 | 5 | 9 |
procedure TForm1.Button1Click(Sender: TObject);
var
pMi_Byte: ^Byte;
begin
if (nil <> pMi_Byte) then // <-- Aquí se produce el "warning"
begin
// Hacer algo ...
end;
end;
¿Qué sucede en el momento de la declaración de la variable 'pMi_Byte'? Pues que, como antes,
el compilador (el sistema operativo, mejor) adquiere 4 celdas de memoria (4 bytes) y sitúa ahí la
variable. Dada la disposición de memoria de los esquemas anteriores, podría resultar así:
F E D C B A 9 8 7 6 5 4 3 2 1 0
| 4 | 6 | 1 | 0 | 3 | 5 | 9 |
donde las 'Y' muestran la posición de memoria en que se ha situado la variable 'pMi_Byte'. Por lo visto,
las celdas C y D están en blanco (es decir, tendrán cero como valor) pero las celdas A y B tienen
los valores 6 y 4 respectivamente. Si seguimos con ese ejemplo, tenemos que la variable 'pMi_Byte' apunta a una
posición de memoria que está situada en $406 (hexadecimal), sea lo que sea que haya ahí.
Así que esa posición de memoria ($406) vuelve a ser "basura", es decir, algo que no se
sabe qué contiene.
Para evitar el aviso ("warning") y, de paso, evitar que se haga algo en una parte de la memoria en la
que no sabemos qué hay, es necesario, como antes INICIALIZAR EL PUNTERO. Eso se consigue haciendo que "apunte"
a algo conocido. Un ejemplo de ello podría ser:
procedure TForm1.Button1Click(Sender: TObject); var Mi_Byte: Byte; // Declarar variable de tipo predefinido pMi_Byte: ^Byte; // Declarar variable de tipo puntero begin Mi_Byte := 8; // Inicializar la variable pMi_Byte:= @Mi_Byte; // Inicializar puntero if (nil <> pMi_Byte) then begin // Hacer algo... end; end;
La asignación que se hace en
pMi_Byte := @Mi_Byte;
puede leerse como "asignar a 'pMi_Byte' la dirección de memoria en la que esté la variable
'Mi_Byte'". Como antes con el acento ('^'), lo de la arroba ('@') es culpa del señor Niklauss Wirth,
"inventor" del lenguaje.
Visto de manera esquemática, podría ser algo así:
var Mi_Byte: Byte; // <-- Aquí 'Mi_Byte = 3' pMi_Byte: ^Byte; // <-- Aquí 'pMi_Byte = $406'
F E D C B A 9 8 7 6 5 4 3 2 1 0
| 4 | 6 | 1 | 0 | 3 | 5 | 9 |
Mi_Byte := 8; // Inicializar la variable
F E D C B A 9 8 7 6 5 4 3 2 1 0
| 4 | 6 | 1 | 0 | 8 | 5 | 9 |
pMi_Byte:= @Mi_Byte; // <-- Aquí 'pMi_Byte = $7'
F E D C B A 9 8 7 6 5 4 3 2 1 0
| 7 | 1 | 0 | 8 | 5 | 9 |
Una vez tenemos el puntero inicializado, podemos variar el valor de la celda a la que apunta, de la misma manera que haríamos con una variable del mismo tipo del puntero. A ver si se entiende:
procedure TForm1.Button1Click(Sender: TObject);
var
Mi_Byte: Byte; // Declarar variable de tipo predefinido
pMi_Byte: ^Byte; // Declarar variable de tipo puntero
begin
Mi_Byte := 8; // Inicializar la variable
pMi_Byte:= @Mi_Byte; // Inicializar puntero
if (nil <> pMi_Byte) then
begin
pMi_Byte^ := 3; // Cambiar el valor
// Hacer algo ...
end;
end;
Al hacer la asignación
pMi_Byte^ := 3;
se está cambiando el valor de la celda a la que apunta 'pMi_Byte' o, lo que es lo mismo, cambiando el
valor de la variable 'Mi_Byte'. Visto de manera esquemática y siguiendo el ejemplo anterior, sería:
ANTES
F E D C B A 9 8 7 6 5 4 3 2 1 0
| 7 | 1 | 0 | 8 | 5 | 9 |
DESPUÉS
F E D C B A 9 8 7 6 5 4 3 2 1 0
| 7 | 1 | 0 | 3 | 5 | 9 |
Queda la duda de para qué necesita un puntero saber el tipo al que apunta. Al fin y al cabo, una celda (byte)
es igual a otra celda (byte).
En el ejemplo con el que estamos jugando la respuesta es sencilla: para saber el número de celdas (bytes)
que debe saltar el puntero cuando se necesite incrementar o decrementar. Lo veremos con un ejemplo.
En el siguiente ejemplo se asigna a un "puntero a byte" la dirección en que se encuentra un entero
(Integer). Una vez asignado, apuntará al "byte de orden inferior" del entero (como se sabe, los
enteros tienen 4 bytes o celdas de memoria y cada byte tiene 8 bits). Para que sea fácil seguir las modificaciones
que se realizan a través del puntero, pongo en binario el número en cada uno de los pasos que van
en el siguiente ejemplo:
procedure TForm1.Button1Click(Sender: TObject); var pMi_Byte: ^Byte; ii: Integer; begin // Byte 3 | Byte 2 | Byte 1 | Byte 0 ii := 26; // 0000.0000.0000.0000.0000.0000.0001.1010 = 26 decimal pMi_Byte := @ii; // <-- Inicializar puntero pMi_Byte^ := 4; // 0000.0000.0000.0000.0000.0000.0000.0100 = 4 decimal Inc(pMi_Byte); // <--------|---------|---------|--------- Siguiente byte pMi_Byte^ := 3; // 0000.0000.0000.0000.0000.0011.0000.0100 = 772 decimal Inc(pMi_Byte); // <--------|---------|---------|--------- Siguiente byte pMi_Byte^ := 7; // 0000.0000.0000.0111.0000.0011.0000.0100 = 459524 decimal Inc(pMi_Byte); // <--------|---------|---------|--------- Siguiente byte pMi_Byte^ := 1; // 0000.0001.0000.0111.0000.0011.0000.0100 = 17236740 decimal // Aquí 'ii = 17236740' end;
Lógicamente, el ejemplo puesto es de lo más tonto porque se podría haber asignado directamente
el valor 17236740 a la variable 'ii': al finalizar la última asignación, será ese el valor
que tenga, tal y como se dice en el comentario final.
Los punteros sirven para realizar cosas muy útiles. Sin embargo, hay que tratarlos bien, esto es, con cuidado.
Por ejemplo, si tras la última línea se hubiese vuelto a incrementar el puntero ('Inc(pMi_Byte)')
la dirección a la que apuntaría entonces estaría fuera del entero. Y "fuera del entero"
quiere decir que podría ser una celda con datos vitales para la aplicación o para el propio sistema
operativo. Escribir entonces en esa celda podría invalidar el valor de la celda lo que, sin duda, producirá
una excepción
que puede o no ser visible (en ocasiones "azulmente" visible).
Mario Rodríguez
El Rinconcito Informático: 25/06/2000 - (c) 2000 - 2008 | Creación y mantenimiento : José Luis Freire | Se pretende poder utilizar cualquier navegador. Recomendado 1024x768 |