miércoles, 31 de julio de 2024

Programación del Sinclair QL (XXII): Variables

Ampliado el 04/09/2024 en azul y el 05/09/24 en rojo


Retomo tras varios años la serie de programación de mi querido QL, seguiremos con los árboles, pero antes hablemos de conceptos básicos sobre punteros, necesarios para entender como montar los árboles.

Aunque esta entrada entrá en la serie del QL, realmente es general sobre programación, debes entender estos conceptos ya que son válidos para cualquier lenguaje, y en la siguiente hablaré sobre punteros.

Compiladores e Intérpretes

Escribimos nuestros programas en algún lenguaje, en donde usamos variables como un sistema para almacenar los datos que manipulemos y la lógica que usamos para su manipulación. Para ejecutar el programa podemos usar dos sistemas:

  • En BASIC normalmente se usa un interprete, que va leyendo el programa fuente línea a línea y ejecutándola, esto es bastante simple y necesita poco espacio en memoria.
  • Los compiladores en cambio convierte el programa fuente en otro que reconozca la máquina (normalmente código máquina, pero en algunos como JAVA generan un código intermedio que luego se procesan usando un ejecutor o una máquina virtual). Para ello se usa un programa que va leyendo el programa y ejecutando el proceso en una o varias pasadas, montando una serie de tablas para poder manejar cosas que todavía no se han definido. Esto requiere mas memoria para disponer del compilador, del buffer donde almacenar el programa que vamos leyendo, las tablas auxiliares, etc.

Variables

En ambos casos se necesita reconocer las variables, de forma que siempre que encontremos una podamos usar su valor correcto. Para ello en ambos sistemas se crea una tabla de variables, en donde se almacenan los datos necesarios para manejarlas, lo que se denominan atributos. En esta tabla se incluyen varios atributos importantes:

  • Nombre: El nombre que le damos a una variables para nosotros es su principal atributo, ya que es con el que interactuamos con ellas. Su longitud es muy variable entre lenguajes, en los primeros BASIC constaba de una letra o de una letra y un solo dígito (A, B$, C2, D4 ...), en los TRS-80 su BASIC permitía nombre largos, pero solo consideraba los dos primeros (lo que solo es un poco mejor), y por experiencia personal se puede programar así solo requiere disciplina. En lenguajes antiguos se comenzaron usando hasta 6, luego se aumentó a 8 (bastantes años más tarde, y en las primeras versiones del ABAL, un dialecto del BASIC usado por BULL, se mantenían ilimitados pero solo consideraba los 8 primeros), rápidamente creció el tamaño hasta 256, y hoy día podemos decir que los lenguajes modernos tienen longitud ilimitada. Los nombres deben comenzar por una letra, o por el símbolo de subrayado más una letra. Hay lenguajes que distinguen mayúsculas y minúsculas en los nombre (lo que ocasiona errores si te equivocas, por lo que se recomienda usar siempre mayúsculas o minúsculas).
  • Tipo de Contenido: En los lenguajes antiguos teníamos variables numéricas de tipo entero o de tipo decimal, y variables alfanuméricas, de manera similar a lo que soportan los propios ordenadores internamente, luego se han ido ampliando los tipos:
    • Alfanuméricas: Almacenan caracteres alfabéticos, caracteres numéricos y símbolos especiales (espacio, punto, +, *, &, etc.). En general hay estos tipos:
      • Carácter o char: Un solo caracter
      • Cadena o string: Una lista de caracteres, que puede ser de longitud fija (tienen un máximo que no se puede sobrepasar, hay lenguajes que rellenan con espacios por la derecha y otros que no lo hacen, marcando el final de la cadena), o de longitud variable (la cadena ocupa su longitud al crearla, si se amplia su contenido se amplia la cadena a su vez).
En el QL las variables de cadena se definen añadiendo el símbolo $ al nombre de la variable, y almacenan valores de 8 bits en código ASCII, aunque dependiendo de la ROM usará una configuración regional diferentes para algunos caracteres locales, como la Ñ o la £.
    • Enteras: Un valor numérico sin decimales, son los más rápidos de manejo por lo que debe priorizarse su manejo. Internamente se guardan en binario, usando o bien un bit para el signo o bien usando complementos (esto lo explicaré mas adelante), su representación interna siempre es exacta, pero están limitados en rango por su tamaño, así en 8 bits y usando uno para el signo, disponemos de valores entre -127 a +127 (realmente -128 a 127, cosas del binario, pasará lo mismo con el resto de tamaños), con 16 bits ampliamos a ±32.767, con 32 bits alcanzamos ±2.147.483.647, con 64 bits ±9.223.372.036.854.775.808 (ojo, el punto es el separador de miles que usamos en España, en otros países serían comas). Realmente hay que
El tamaño del entero es el base del procesador, en máquinas antiguas serían 8 bits, hoy día es de 64, y se han definido muchas variantes:
      • Entero: Por defecto del tamaño de registro del procesador (8,16,32,64)
      • Entero sin signo: Si no necesitamos números negativos, con este tipo ampliamos en 1 bit el tamaño, por lo que con 8 bits serían de 0 a 256, con 16 bits de 0 a  65.536, etc.
      • Entero corto y largo: Cuando las máquinas eran de 8 bits no había otra opción, pero al ir ampliando el tamaño a 16, para reducir ocupación en memoria que era un recurso muy limitado siempre, se definieron enteros cortos que ocupaban la mitad, y enteros largos que ocupaban el doble.
      • Carácter: Representa el valor numérico de un carácter alfanumérico en el código ASCII, pero en ciertos lenguajes podemos usarlo como un entero corto de 8 bits (o 16 si es en Unicode), soportando también su uso con o sin signo. 
      • Booleanas: Un entero en donde 0=Falso y 1=Cierto, o más generalmente cero falso y diferente a cero cierto. Ocupan un byte completo para solo usar un bit, lo que en máquinas de 64 bits es mucho espacio desaprovechado.
      • Fecha, hora, fecha y hora: Son valores enteros que guardan realmente el número de días desde una fecha inicial dada, la hora del día en décimas de segundo, o ambos simultáneamente en dos enteros.
      • Punteros: Esto lo veremos luego.
      • De coma fija: Estos son números que tienen una cantidad de decimales predefinida, y su valor se guarda directamente en binario como un entero, redondeando el valor a los decimales que aceptan, ajustando los decimales truncando o añadiendo ceros por la derecha para alcanzar los definidos, y luego eliminando el punto decimal, así serán enteros y de esta manera se pueden sumar directamente entre sí, añadiendo ceros por la derecha si es necesario igualar el número de los decimales entre las variables (esto es muy rápido en los ordenadores).
En el QL las variables enteras se definen añadiendo el símbolo % al nombre de la variable, y almacenan valores en 16 bits, entre -32.768 y +32.767.
    • Decimales: Un valor numérico que admite decimales. Internamente se guarda con una codificación científica, del tipo smEn, donde s es el signo, m la mantisa (se guarda el valor completo como todos sus dígitos sin punto decimal), y n sería el exponente (donde se ubica el signo decimal). Hay varios tipos según su tamaño, lo que cambia es la cantidad de cifras significativas de la mantisa y del exponente.:
      • La codificación normalizada IEEE 754 ocupa 32 bits, denominándose de simple precisión
      • Las de media precisión ocupan 16 bits
      • Las de doble precisión ocupan 64 bits
      • Las de cuádruple precisión o doble precisión largas, con 128 bits
      • Hay hasta las de octuple precisión con 256 bits.
En el QL las variables decimales se definen sin añadir nada a su nombre, y almacenan valores en 16 bits, su valor se presentará como un número si no es excesivamente grande, o con formato científico en caso contrario. 
    • Tipos especiales: Aquí cada lenguaje tiene sus variantes, podemos hablar en general de:
      • Puntero:  que guarda una posición de memoria, es el objetivo de esta entrada
      • Tipo indefinido (void): representa que no usaremos su valor por lo que solo nos interesa su nombre (en C una función se comporta como una variable, ya que dispone de su entrada en la tabla de Variables, las funciones son las que retornan un valor, los procedimientos no las retornan y se definen de tipo void)
      • Cualquier tipo (any): que indica que guardaremos cualquier posible valor en la variable (usar con cuidado ya que ralentiza mucho el programa). Son usuales en JavaScript.
      • Objeto: realmente son punteros, pero que apuntan a una estructura en memoria compleja que dispone de código y datos.
El tipo de contenido representa un tamaño en la memoria. Hay lenguajes fuertemente tipados, que no permiten asignar una variable de un tipo a una de otro tipo, y lenguajes pobremente tipados que hacen conversiones automáticas entre los tipos de las variables (esto por si solo requiere una entrada para ello, ya veremos si algún día la hago). NOTA: Nuestro QL es pobremente tipado, por tanto podemos asignar a una numérica una cadena, e intentará hacer la conversión a partir de su contenido. Si escribís a$="12.45 MAS" : b=a$ : c%=a$ : PRINT a$, b, c% veréis que en pantalla aparece   12.45 MAS       12.45       12

  • Ubicación en memoria: Todos los datos deben estar ubicados en algún lugar de la memoria del ordenador, en general se reserva una zona denominada "pila del programa" o de forma similar, en donde se guardan todas las variables que se utilicen. Para variables simples apuntará directamente a donde está su valor, pero para variables mas complejas apunta al inicio de la zona en que está ubicada. Para crear la entrada para un nuevo valor, se mira la posición de la última entrada y se le suma la ocupación, así tenemos la ubicación de la nueva variable. Si fuera la primera variable y por tanto la tabla está vacía, se rellena con la dirección base de la "pila del programa".
Desde nuestro programa no vemos esa tabla de variables ni sabemos nada de direcciones de memoria, realmente solo nos interesa conocer el VALOR que es el contenido de la variable, realmente unas posiciones concretas en la memoria apuntadas por la tabla anterior. En la mayoría de lenguajes su valor se inicializa al comenzar el programa a un valor por defecto, cero para numéricas (en booleanas cero equivale a false, en fechas el valor sería 0, pero como representa los días que transcurren desde una fecha base preestablecida sería el valor de esa fecha), las cadenas se inicializan como vacías o con espacios según el lenguaje.
 
Hay que tener cuidado con lenguajes donde las variables no se inicializan como C o C++ , tendrán un valor aleatorio según lo que haya en ese momento en la memoria, por lo que se siempre se recomienda darles el valor inicial al definirlas en la forma int a=0; float b=0.0; char Saludo[5]="Hola";
 
En nuestro QL el sistema utiliza tres listas separadas para el manejo de las variables, procedimientos y sus nombres:
  • Por un lado tenemos la lista de nombres de las variables, procedimientos y funciones. La primera vez que las definimos se guarda en esta lista, y a partir de entonces al escribir su nombre el sistema lo ajustará para que coincida con lo que has escrito en cuanto a mayúsculas y minúsculas en las líneas. Esto hace que si queremos cambiar la variable nododerecho por NodoDerecho el sistema no nos deje, pues ya está en la lista.
  • Otra lista contiene los tipos de cada variable, procedimiento o función, para las locales incluye el procedimiento o función en que tienen vigencia, junto al puntero en la memoria donde está ubicado su valor.
  • Una tercera es la que contiene los valores de las variables.

Definición explicita de variables

En BASIC o Python no es necesario definir las variables antes de su uso, son lo que se denomina lenguajes implícitos, conforme avanza el programa se va rellenando la tabla de variables cuando se localiza una nueva, esto puede ocasionar problemas ya que si nos equivocamos en el nombre, el sistema crea una nueva variable sin que nos demos cuenta, lo que provocará un error que habitualmente cuesta localizar.
 
En contra la mayoría de lenguajes son de tipo explícito, obligan a definir una variable antes de poder usarla, algunos como en COBOL separadas del código ejecutable, otros como Pascal C (y los que usan su sintaxis como C++, C#, Java o PHP) se pueden definir en cualquier momento. Al definir las variables, si nos equivocamos en el nombre dará un error por variable no definida.

Hay algunos lenguajes que se pueden configurar por la configuración del entorno de programación o usando una instrucción de pre-procesador en el propio programa para que se comporten de una manera o de la otra.

Variables globales y variables locales

En el BASIC estándar todas las variables son globales, eso significa que las podemos usar en cualquier parte del programa sin problema. Evidentemente en lenguajes donde definimos las variables antes de comenzar el programa en un bloque separado, como COBOL o en el ABAL de Bull, todas las variables son también globales.
 
En lenguajes que soportan funciones o procedimientos, existe la posibilidad de definir variables que solo se verán dentro de ellas, desapareciendo su valor al salir de las mismas (aunque en algunos se conservan los valores, perdiendo entonces parte de su ventaja). Estas son variables locales.

La idea de las locales surgió en ALGOL, donde existen bloques del programa, estos bloques se marcan con una instrucción de inicio y otra de fin, por ejemplo en C se usan las llaves {} mientras en PASCAL se usa begin y end. Dentro de estos bloques también podemos definir variables locales a los mismos.

Una variable local pueden usar el mismo nombre que una global, siendo ambas independientes, en la función se usará la variable local, y fuera de ella se usará la global, pero esto puede dar lugar a errores dentro de nuestras funciones por pensar que estamos usando la global, por eso hay que ser cuidadosos en el nombre de las variables. Para evitar errores es buena costumbre usar una letra antes del nombre, sobre todo en las locales, por ejemplo añadir loc delante.
 
Un uso muy habitual de una variable local en C es para los bucles definiendo las variables índice dentro del propio bloque, o incluso en la definición del mismo, por ejemplo for(int i=1;i<10;i++) { ... } esto es un bucle en donde i tendrá los valores del 1 al 9, y después del mismo desaparece. Veamos un ejemplo un poco mas completo:

(1)    int i=84;
(2)    do {
(3)        int i=6;
(4)        i++;
(5)        printf("%d", i);
(6)    } while (i<10) 
(7)    printf("%d", i);
 
La línea 5 presentará los valores de la variable i que es local al bucle: 6,7,8,9 y luego en la línea 7 presentará la variable global con valor 84.
 
En nuestro querido SuperBASIC se permie definir variables locales en las funciones y procedimientos, usando el comando LOC seguido de las variables que queramos definir. Por ejemplo
 
10 a=1 : b$="A"
20 print "1",a,b$
30 prueba
40 print "3",a,b$
50 Define PROCedure prueba
60     loc a,b$
70     a=5 : b$="U"
80    print "2",a,b$
90 End DEFine 
 
Al ejecutarlo imprimirá:
1   1   A
2   5   U
3   1   A 

En la siguiente entrada abordamos los punteros, de uso muy habitual en C, pero fuente de problemas por lo que se prefiere ocultados en otros lenguajes como Java, que los usan implícitamente sin que los veamos.


1 comentario: