Diccionario Informático Ampliado

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
                               

Empleo números hexadecimales porque es así como nos va a presentar la memoria el depurador de Delphi. También numero de derecha a izquierda porque, en general, esa será la representación de los números que se encontrará en los libros de programación. Lógicamente, tras la celda F de la izquierda vendrán otras celdas, hasta el total de memoria de la máquina.

Cada una de las celdas (0, 1, 2,..., F,...) tiene el tamaño de un BYTE que, como decía antes, es la unidad mínima de información a la que puede accederse en una máquina 386 (en otras máquinas esto puede variar) que es sobre la que trabajaremos.

En algún momento de nuestra aplicación, en el código de la misma, vamos a necesitar DECLARAR UNA VARIABLE. El término de VARIABLE significa que su valor puede "variar" a lo largo de la ejecución.

En Delphi, para DECLARAR una variable, se utiliza la "palabra reservada" 'var'. Una "palabra reservada" no puede ser utilizada en otro lugar ni para otros propósitos distintos a los que hayan decidido los que han diseñado el lenguaje. Así, por ejemplo, 'var' sólo puede ser utilizada para DECLARAR VARIABLES.

Por simplificar, vamos a suponer que deseamos DECLARAR una variable de tipo BYTE. Lo haríamos de la siguiente manera:

var
Mi_Byte: Byte;

Eso, declara la variable de nombre 'Mi_Byte' y de tipo 'Byte'. Ahora bien, ¿qué sucede en la memoria cuando hago esa declaración? Que el compilador "reserva" tantas celdas de memoria como sean necesarias para almacenar un Byte que, como hemos dicho, será una celda.

Pero, ¿cuál es la celda reservada? Ni idea. Es decir, sólo puede saberse la celda reservada cuando el flujo de ejecución de la
aplicación pase por el lugar en que se declara la variable 'Mi_Byte'.
Llegados a ese punto, quizá viésemos la memoria así:

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            

Los valores de las celdas no tienen ningún significado, excepto quizá para la aplicación que los ha estado utilizando: de ahí viene el término de "basura". Es algo que no puede ser utilizado por otras aplicaciones ya que no se sabe qué significan esos valores.

Si ahora volvemos al caso que comentábamos, es decir, a la declaración de 'Mi_Byte', es claro que, en el punto de la declaración, la variable tomará el valor 3, esto es, el contenido de la celda 7 donde la ha situado el compilador (en la realidad, es el sistema operativo el que lo hace).

Si la declaración de la variable está dentro de un procedimiento y se intenta utilizar, antes de darle un valor, el compilador avisará con un "warning" (aviso) que dice "Variable 'Mi_Byte' might not have been initialized" que, más o menos, viene a significar que no se le ha dado un valor a 'Mi_Byte'.
Sería el caso siguiente:

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            


DESPUÉS DE LA INICIALIZACIÓN X <-- aquí va 'Mi_Byte'
F E D C B A 9 8 7 6 5 4 3 2 1 0
      4 6 1 0 8 5 9            

PUNTEROS

En ocasiones necesitaremos hacer referencia a una posición de memoria, sea donde sea que esté esa posición. No es éste el mismo proceso que cuando necesitamos reservar una cantidad de celdas para almacenar un valor. Lo que ahora necesitamos ahora es "apuntar" a ese conjunto de celdas. ¿Que para qué? Lo veremos.

Un apuntador o PUNTERO es una variable que tiene, por valor, una dirección de memoria. Como antes, al ser "variable" es posible cambiar su valor de tal manera que en un momento determinado contenga una dirección de memoria y, en otro, otro distinto.

En Object Pascal, para declarar un puntero se utiliza el acento circunflejo (un lío de mil demonios: echarle la culpa a Niklauss), esto es, el símbolo '^'.

Hay que hacer notar que un puntero siempre va a tener un tamaño de 32 bits (4 bytes o celdas de memoria), apunte a lo que apunte, ya que lo que contiene un puntero (su valor) es una dirección de memoria y, en las máquinas 386 y, en concreto, en Win32 (Windows, a partir de la versión 95) las direcciones de memoria tienen 32 bits o 4 bytes (al gusto de cada cual).

Como con las variables "normales", para DECLARAR UN PUNTERO se emplea la palabra reservada 'var', seguida del nombre que se le quiere dar a la variable y, a continuación, el tipo de dato al que apuntará. Por ejemplo:

var
pMi_Byte: ^Byte;

Eso declara una variable de nombre 'pMi_Byte' que apunta a "algo" que tiene de tamaño un byte. Yo suelo leer eso así: "declaro una variable de nombre 'pMi_Byte' que apunta a ('^') un byte".

Quizá la pregunta que haya que hacerse es para qué necesitamos saber a qué tipo de dato apunta (y, realmente, es una buena pregunta que, espero, se aclarará más abajo).

Como con las variables de los tipos predefinidos, una variable puntero no tendrá un valor válido hasta que no se lo demos. Es decir, hay que realizar el proceso de INICIALIZACIÓN antes de poder usar la variable. Aquí, además, de no hacerlo, podemos obtener unos errores de lo más aparatosos, tipo los de pantalla azul, muy típicos de Windows. Ya veremos por qué.

En el siguiente código obtenemos un aviso ("warning") similar al que nos daba antes la falta de inicialización de la variable 'Mi_Byte'. El aviso, en este caso, dice "Variable 'pMi_Byte' might not have been initialized". Es decir, el mismo que antes, excepto porque el nombre de la variable es otro:

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            
- Y Y Y Y

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            
- Y Y Y Y X

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            
- Y Y Y Y X

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            
- Y Y Y Y X

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            
- Y Y Y Y X


DESPUÉS

F E D C B A 9 8 7 6 5 4 3 2 1 0 

      7 1 0 3 5 9            
- Y Y Y Y X


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