Descargar

Manual de FreePascal (Parte 1) (página 4)

Enviado por rafaelfreites


Partes: 1, 2, 3, 4, 5, 6

Es posible emplear los mismos nombres en ámbitos diferentes pero no es recomendable a menos que estemos muy seguros de que no nos vamos a confundir.

Sobrecarga de procedimientos y funciones

Es posible definir más de una función o procedimiento con el mismo identificador pero con parámetros distintos. Esta característica permite ahorrar trabajo al programador ya que con un mismo nombre puede englobar más de una función que realice operaciones parecidas, por ejemplo.

Para sobrecargar una función o procedimiento sólo hay que redeclararlo y tener en cuenta de que tienen que cambiar los parámetros. Si no cambian los tipos de los parámetros el compilador no sabrá distinguir las dos funciones y presentará un error de compilación.

Vamos a implementar tres funciones sobrecargadas que compararan pares de datos de tipo string, enteros y booleanos. Las funciones devolverán cero cuando los dos elementos sean iguales, 1 cuando el primero sea mayor y -1 cuando el segundo sea mayor que el primero.

function Comparar(a, b : integer) : integer;

begin

if a > b then Comparar := 1;

if a < b then Comparar := -1;

if a = b then Comparar := 0;

end;

function Comparar(a, b : string) : integer;

begin

if a > b then Comparar := 1;

if a < b then Comparar := -1;

if a = b then Comparar := 0;

end;

function Comparar(a, b : boolean) : integer;

begin

if a > b then Comparar := 1;

if a < b then Comparar := -1;

if a = b then Comparar := 0;

end;

En este caso el código de las tres funciones coincide pero no tiene porque ser así en otros casos. Podemos llamar a las funciones especificando dos enteros, dos strings o dos booleanos pero no ninguna otra combinación que no hayamos especificado. Incluso, podemos sobrecargar métodos con distinto número de parámetros. Por ejemplo :

function Comparar(a : integer) : integer;

begin

Comparar := Comparar(a, 0);

end;

En este caso si no especificamos el segundo entero entonces la comparación se hará con cero. Suele ser habitual definir métodos sobrecargados con menos parámetros cuando se trata de casos más habituales o con parámetros por defecto.

La flexibilidad de la sobrecarga en FreePascal permite incluso a admitir la mezcla de funciones y procedimientos. Podemos definir otro procedimiento Comparar en el cual el resultado se guarde en el parámetro c.

procedure Comparar(a, b : integer; var c : integer);

begin

if a > b then c := 1;

if a < b then c := -1;

if a = b then c := 0;

end;

En este caso Comparar no devuelve nada pues es un procedimiento y se empleará el parámetro c para guardar el resultado.

Pasar parámetros de tipo open array

FreePascal admite un tipo especial de parámetros que reciben el nombre de array abierto, open array, que permite especificar un array de mida variable y tipo determinado como parámetro de una función. Para hacerlo hay que emplear la sintaxis array of tipoDato al parámetro en cuestión.

Vamos a implementar una función que escriba una serie de enteros pasados como parámetros.

procedure EscribirEnteros(n : array of Integer);

var

i : integer;

begin

for i := Low(n) to High(n) do

begin

Write(' ', n[i]);

end;

end;

Obsérvese que tenemos que emplear High y Low pues no sabemos el tamaño del array pasado como parámetro. Los datos que podemos pasar a estos parámetros son literales, constantes y variables. Los literales tienen una sintaxis parecida a los literales de conjuntos. Podemos llamar la función con el literal siguiente :

EscribirEnteros([1, 2, 8, 3]);

Esto escribiría en la pantalla :

1 2 8 13

También podemos pasar una variable como parámetro. En este caso tendrá que ser un array de tamaño fijo.

var

prueba : array[1..3] of integer;

begin

prueba[1] := 4;

prueba[2] := 2;

prueba[3] := 6;

EscribirEnteros(prueba);

end.

No hay ningún problema por pasar parámetros por referencia o constantes. Por ejemplo en el caso siguiente se llena el array con el valor del índice al cuadrado.

procedure Cuadrado(var n : array of Integer);

var

i : integer;

begin

for i := Low(n) to High(n) do

begin

n[i] := i*i;

end;

end;

Al ser un parámetro por referencia no podremos pasar literales pero si variables. Es muy recomendable emplear parámetros constantes siempre que sea posible pues la función o procedimiento tardará menos tiempo en ejecutarse.

No hay que confundir los parámetros array abiertos con los array cerrados. En el caso siguiente se puede apreciar la diferencia.

program ArraysAbiertos;

type

TArray3 = array[1..3] of Integer;

procedure Cuadrado(var n : array of Integer);

var

i : integer;

begin

for i := Low(n) to High(n) do

begin

n[i] := i*i;

end;

end;

procedure Cuadrado3(var n : TArray3);

var

i : integer;

begin

for i := Low(n) to High(n) do

begin

n[i] := i*i;

end;

end;

var

prueba : array[1..4] of integer;

begin

Cuadrado(prueba); // Correcto

Cuadrado3(prueba); // Incorrecto pues prueba no es del tipo TArray3

end.

Si el array prueba hubiera sido de 3 elementos y de índice [1..3] (pero no [0..2] u otros) el compilador nos hubiera permitido pasarlo a Cuadrado3. En el caso de los arrays fijos no es posible pasar como parámetro un literal con la sintaxis de los arrays abiertos aunque el parámetro sea constante o por valor. De hecho, en funciones y procedimientos con parámetros de tipo array fijo sólo es posible pasar variables.

Funciones declaradas a posteriori

En algunos casos muy extraños podemos necesitar que dos funciones se llamen la una a la otra de forma recíproca. El problema se encuentra en que si declaramos A antes que B entonces A no puede llamar a B y viceversa. Para resolver este problema podemos posponer la implementación de la función a un punto inferior de forma que al existir la declaración sea sintácticamente correcto llamarla y el compilador permita la llamada mutua de funciones.

Para indicar al compilador de que una función será declarada más abajo hay que emplear la palabra forward después de su declaración Vamos a implementar un par de procedimientos llamados Ping y Pong que se irán pasando un entero que decrecerá. El ejemplo carece de utilidad práctica alguna.

procedure Ping(n : integer); forward; // Vamos a implementarlo más abajo

procedure Pong(n : integer);

begin

if n > 0 then

begin

Writeln('Pong ', n);

Ping(n-1); // Llamamos a Ping

end;

end;

procedure Ping(n : integer); // Implementamos Ping

begin

if n > 0 then

begin

Write('Ping ', n, ' ');

Pong(n); // Llamamos a Pong

end;

end;

begin

Ping(5);

end.

El resultado del programa sería :

Ping 5 Pong 5

Ping 4 Pong 4

Ping 3 Pong 3

Ping 2 Pong 2

Ping 1 Pong 1

 

Es importante implementar, en algún punto del programa posterior a la declaración forward, la función o procedimiento. En caso contrario el compilador indicará un error sintáctico. Tampoco es posible declarar una misma función doblemente forward aunque tampoco tenga ningún sentido hacerlo.

La función exit

Muchas veces nos interesará salir de una función antes de que ésta termine o que no ejecute más instrucciones. Sobretodo cuando en un punto ya conocemos el resultado y no hay que realizar nada más. En estos casos podemos salir de la función o procedimiento con la orden Exit. Obsérvese, que después de llamar a Exit ya no se ejecuta ninguna instrucción más de la función o del procedimiento. No hay que llamar a Exit dentro del bloque principal puesto que se terminaría el programa, lo cual no suele ser recomendable.

function EsPrimo(n : integer) : boolean;

{ Devuelve true si n es primo, false en caso contrario Esta función es poco eficiente y es un ejemplo del uso de Exit }

var

i : integer;

begin

if n < 0 then n := Abs(n); // Trabajaremos con positivos

if n = 1 then

begin

// En el número 1 no tiene sentido hablar de primalidad

EsPrimo := false;

Exit; // Salimos de la función

end;

// Comprobamos si es primo

for i := 2 to (n-1) do

begin

if n mod i = 0 then

begin // Es divisible por lo que no es primo

EsPrimo := false;

Exit; // Salimos de la función

end;

end;

// Si llegamos aquí es porque no se ha dividido con los anteriores

EsPrimo := true; // por tanto es primo

end;

Devolver al estilo de C/C++

Las funciones de C y C++ devuelven un valor mediante la palabra reservada return seguida de una expresión. Esto hace que la función termine y devuelva el valor de la expresión. En Pascal, la asignación al identificador de la función no hace que ésta termine por lo que muchas veces necesitaremos añadir un Exit después de la asignación. FreePascal extiende la función Exit con un parámetro que es el valor que devolverá la función.

Por ejemplo, podemos simplificar las dos instrucciones siguientes :

EsPrimo := false;

Exit; // Salimos de la función

en la siguiente :

Exit(false);

Esta sintaxis es mucho más parecida a la orden return de C/C++ y facilita el trabajo a la hora de portar funciones de C a Pascal.

Recursividad

Ventajas de la recursividad

Muchas operaciones requieren procesos repetitivos y se pueden implementar con sentencias del tipo for, while o repeat. Algunos casos, pero, no se pueden diseñar con sentencias iterativas puesto que ya no es posible o es muy complejo. Como alternativa disponemos de una herramienta muy potente, a la par que arriesgada y compleja, como es la recursividad.

Donde tiene más utilidad la recursividad es en procesos que podríamos llamar inductivos. Dada una propiedad P(n), donde n pertenece a los enteros, que se cumple para n0 y que si se cumple para un n cualquiera implica que también se cumple para n+1, entonces es muy probable que se tenga que codificar mediante recursividad.

Qué es recursividad ? Hablamos de recursividad cuando una función se llama a si misma, con parámetros distintos, para llegar a un caso en el cual no haya que segur llamándose a si misma y se pueda resolver la llamada inicial.

La ventaja de la recursividad es poder escribir algoritmos muy eficaces y rápidos con pocas líneas de código.

Un ejemplo sencillo : el factorial

Sabemos que la operación factorial n! equivale a n! = n (n-1) (n-2)…2 1 y 0! = 1. Una forma de codificación sin emplear la recursividad es la siguiente :

function Factorial(n : integer) : Integer;

var

i, temp : integer;

begin

temp := 1;

for i := 1 to n do

begin

temp := i*temp;

end;

Factorial := temp;

end;

Si queremos codificar el factorial de forma recursiva hay que tener en cuenta el caso sencillo cuando n = 0, 0! = 1, y que n! = n (n-1)! El código siguiente es la forma recursiva del factorial :

function Factorial(n : integer) : Integer;

begin

if n = 0 then

begin

Factorial := 1

end

else

begin

Factorial := n*Factorial(n-1);

end;

end;

Como se ve, siempre llega algún momento en el cual ya no hay más llamadas recursivas puesto que n se vuelve cero en algún momento (siempre la n inicial sea mayor que cero, claro).

Es importante que nuestro código termine en algún momento las llamadas recursivas, pues de no hacerlo la llamada a las funciones sería infinita y gastaríamos toda la memoria dedicada a las variables locales (memoria stack) y se produciría un error runtime llamado Stack Overflow que provocaría que nuestro programa finalizara anormalmente.

Punteros y memoria

Variables estáticas

Hasta ahora todas las variables que hemos empleado se gestionan de forma totalmente automática por el compilador. ..este se encarga de añadir el código necesario para crearlas al iniciar el programa y liberarlas cuando termina. Reciben el nombre de variables estáticas.

En algunos casos necesitaremos acceder directamente a las posiciones de memoria de nuestras variables, en otros necesitaremos zonas de memoria con tamaños especiales, etc. En estos casos tendremos que emplear los punteros.

Estructura de la memoria

La memoria de un ordenador está formada por un conjunto determinado de celdas de un byte a las cuales podemos acceder mediante su dirección. Esta dirección es única para cada posición de memoria y viene expresada, en ordenadores superiores al 386, por variables enteras de 32-bits. De esta forma, teóricamente, se podrían direccionar 232 posiciones de memoria diferentes lo que equivale 4 Gb de memoria RAM. En la práctica no se llega nunca a esta cifra debido a imposibilidades técnicas evidentes.

Punteros con tipo

Un puntero es una variable que apunta a una posición de memoria. Los punteros con tipo se declaran añadiendo el símbolo ^ (circunflejo) antes de su tipo.

var

PunteroEntero : ^Integer;

Antes de poder emplear un puntero con tipo habrá que reservar memoria. Para esto tenemos la función New que devolverá una dirección a la memoria alojada. New se encarga de reservar suficiente memoria para nuestro puntero con tipo.

New(PunteroEntero);

Cuando ya no necesitemos el puntero es necesario liberarlo. De esta forma el sistema se da por enterado de que esta posición de memoria está libre y puede ser ocupada por otros datos. Si no lo liberásemos, el sistema entendería que está ocupado con lo que no intentaría alojar más datos en esta posición y malgastaríamos la memoria. A partir de este momento ya no será válido acceder al contenido del puntero. Para hacerlo emplearemos el procedimiento Dispose :

Dispose(PunteroEntero);

Ya que PunteroEntero sólo apunta a una dirección de memoria será necesario poder acceder a su contenido. Para esto hay que emplear el operador de desreferenciación ^.

PunteroEntero^ := 10;

Obsérvese que es radicalmente diferente la asignación PunteroEntero^ := 10; que la asignación PunteroEntero := 10; En este último caso hacemos que el puntero PunteroEntero apunte a la dirección de memoria 10. Este es un error bastante habitual ya que el compilador autoriza las asignaciones directas al compilador. En este caso es más notable pues el puntero es de tipo entero.

program PunterosConTipo;

var

PunteroEntero : ^Integer;

begin

New(PunteroEntero);

PunteroEntero^ := 5; // Asignamos 5 a la posición apuntada por el puntero

{ PunteroEntero := 5; PELIGRO! El puntero apuntaría a la posición n 5 }

Dispose(PunteroEntero);

end.

Hay que ir con mucho cuidado cuando hacemos asignaciones a punteros sin desreferenciar pues es un error bastante habitual. Además, si el puntero cambia de valor, entonces apunta a otra dirección de la memoria y por tanto el valor de la posición anterior se pierde inevitablemente con lo que no podremos liberar la memoria que habíamos reservado inicialmente pues intentaríamos liberar otra posición y esto es un peligro potencial, pues puede ser memoria que no tenga que ser liberada.

En este ejemplo hemos reservado memoria para un puntero y la hemos liberado. Mediante el operador de desreferenciación hemos modificado el contenido de la memoria direccionada por el puntero.

Si queremos que un puntero no apunte en ningún lugar empleamos la palabra reservada nil.

PunteroEntero := nil;

Puede resultar extraño en un primer momento que un puntero no apunte en ningún lugar. Si intentamos obtener algún valor del puntero mediante PunteroEntero^ obtendremos un error runtime de protección general.

Es importante ver que hasta que no se reserva memoria para un puntero no podemos acceder a su contenido ya que el valor que contiene no apunta en ningún lugar, tiene el valor nil. Podemos comprobar si un puntero no apunta en ningún sitio si su valor es nil.

if PunteroEntero = nil then …

Los punteros del mismo tipo son compatibles por asignación. Además las modificaciones que hagamos en un puntero que apunta a una misma posición apuntada por otro puntero se ven ambos punteros.

program PunterosConTipo;

var

PunteroEntero, PunteroModificar : ^Integer;

begin

New(PunteroEntero);

PunteroEntero^ := 5;

PunteroModificar := PunteroEntero; { Asignamos a PunteroModificar la misma dirección que PunteroEntero }

Writeln(PunteroEntero^); {5}

Writeln(PunteroModificar^); {5}

PunteroModificar^ := 12;

Writeln(PunteroEntero^); {12}

Writeln(PunteroModificar^); {12}

Dispose(PunteroEntero);

{ A partir de aquí PunteroEntero^ i PunteroModificar^ ya no son válidos }

end.

Vemos que modificar el contenido de esta posición mediante uno de los dos punteros produce el mismo resultado ya que los dos punteros se refieren a la misma posición de memoria y por tanto apuntan al mismo valor. Consíderese también el hecho e que liberar una posición de memoria invalida todos los punteros que apunta a ella por lo que PunteroModificar y PunteroEntero ya no pueden ser desreferenciados correctamente. Es importante ver que PunteroModificar no se tiene que liberar con Dispose ya que tampoco hemos reservado memoria para él.

El operador de dirección @

Si empleamos el operador @ delante del nombre de una variable estática automáticamente obtendremos su dirección de memoria. Por tanto podemos asignar este resultado a un puntero del mismo tipo que la variable estática y realizar modificaciones sobre la variable estática mediante el puntero.

program PunterosConTipo;

var

PunteroEntero : ^Integer;

Entero : Integer;

begin

PunteroEntero := @Entero; { Obtenemos su dirección y la guardamos en PunteroEntero }

Entero := 10;

Writeln(Entero); {10}

Writeln(PunteroEntero^); {10}

PunterEnter^ := 12;

Writeln(Entero); {12}

Writeln(PunterEntero^); {12}

end.

Aquí tampoco hay que liberar PunteroEntero ya que no hemos reservado memoria para él. Simplemente lo hemos asignado para que apunte a la dirección de la variable Entero.

Copiar los contenidos de un puntero en otro

Antes hemos visto que al asignar un puntero a otro simplemente hacíamos que apuntase a la dirección asignada pero no se transfería el contenido. Supongamos hemos reservado memoria para los punteros PunteroEntero y PunteroModificar, ambos de tipo Integer.

begin

New(PunteroEntero); // Reservamos memoria para PunteroEntero

New(PunteroModificar); // Reservamos memoria para PunteroModificar

PunteroEntero^ := 10;

PunteroModificar^ := PunteroEntero^; // Copiamos el contenido

Writeln(PunteroModificar^); {10}

Dispose(PunteroEntero);

Dispose(PunteroModificar);

end.

Si en vez de hacer una asignación mediante el operador de desreferenciación hubiésemos hecho la asignación siguiente :

PunteroModificar := PunteroEntero;

No habríamos podido liberar la zona reservada inicialmente para PunteroModificado ya que al hacer Dispose(PunteroModificar) tendríamos un error al intentar liberar una zona de memoria ya liberada. En cualquier otra combinación :

PunteroModificar^ := PunteroEntero;

// o

PunteroModificar := PunteroEntero^;

El compilador no nos habría permitido realizar la asignación pues los elementos no son compatibles por asignación.

Aritmética e indexación de punteros

FreePascal permite realizar aritmética de punteros mediante las operaciones suma y resta y los procedimientos Inc y Dec.

Dados dos punteros con tipo P1 y P2 podemos realizar operaciones como las siguientes :

P1 := P1 + 1; {Ahora P1 apunta un byte más adelante de la dirección inicial}

P1 := P1 – 1; {Ahora P1 vuelve apuntar a la dirección inicial}

P1 := P2 + 2; {Ahora P1 apunta dos bytes más adelante de P2}

P1 := P1 – 2; {Ahora P1 apunta a la dirección de P2}

La función Inc incrementa la dirección del puntero en n bytes donde n bytes es el tamaño del tipo de puntero. Por ejemplo, dado el puntero P de tipo ^Longint entonces :

Inc(P); { Equivale a P := P + 4 }

ya que un Longint tiene un tamaño de 4 bytes. La función Dec hace lo mismo pero en vez de incrementar n bytes los decrementa.

También podemos indexar un puntero como si de un array se tratara. Obsérvese el ejemplo siguiente :

program PunteroArray;

var

PunteroByte : ^Byte;

Entero : Word;

begin

Entero := 18236; { $403C }

PunteroByte := @Entero;

Writeln('Byte de menos peso ', PunteroByte[0]); { $3C = 60 }

Writeln('Byte de mayor peso ', PunteroByte[1]); { $40 = 71 }

end.

En este ejemplo hemos empleado un entero de tipo Word que consta de dos bytes, el de mayor peso o superior, y el de menor peso o inferior, y además no tiene signo. Ya que en la plataforma Intel los bytes de menos peso se almacenan primero, leemos PunteroByte[0] y después PunteroByte[1]. En otras plataformas quizá hubiéramos encontrado primero el byte superior y después el byte inferior. En este caso concreto no es necesario emplear el operador de desreferenciación pues se sobreentiende con la sintaxis de array.

Punteros sin tipo

Es posible emplear un tipo de puntero que no va asociado a un tipo en concreto. Este tipo que recibe el nombre de Pointer permite apuntar a zonas de memoria genéricamente. Si queremos reservar memoria con una variable de tipo Pointer tendremos que emplear GetMem y FreeMem para liberarla. El uso de punteros son tipo es aún más peligroso que con tipo pues no podemos asignar valores al contenido. Las únicas asignaciones posibles son entre otros punteros, con o sin tipo.

program PunteroSinTipo;

var

Puntero : Pointer;

PunteroEntero : ^Integer;

begin

GetMem(Puntero, SizeOf(integer));

PunteroEntero := Puntero;

PunteroEntero^ := 10;

FreeMem(Puntero, SizeOf(integer));

end.

Las funciones FreeMem y GetMem exigen que se les explicite el tamaño que hay que reservar y liberar. En este ejemplo hemos reservado espacio para un entero y hemos asignado su dirección a la variable PunteroEntero con el cual podemos trabajar como si de un entero se tratara. Nótese que se ha obtenido el tamaño del entero mediante SizeOf(integer) y no SizeOf(^Integer) o SizeOf(PunteroEntero) ya que cualquier puntero tiene un tamaño de 4 bytes (32-bits) para poder apuntar a una dirección de memoria.

El tipo AnsiString

El tipo AnsiString es el que se llama cadena larga. No es una cadena propiamente Pascal pero permite superar la limitación de los 255 caracteres impuestos por las cadenas Pascal. Por otra parte tienen un comportamiento especial pues se tratan como punteros por lo que si intentamos escribir en un archivo un AnsiString obtendremos que hemos copiado un entero de 4 bytes y no la cadena en sí.

Cuando una cadena de tipo AnsiString se asigna a un valor vacío () entonces esta toma el valor nil. Cuando le asignamos una variable o un literal compatibles, entonces se reserva suficiente memoria para alojar la cadena. Además, las cadenas AnsiString tienen un contador de referencias interno. Si hacemos la asignación siguiente :

S1 := S2;

Entonces el contador de S2 se reduce a cero para indicar que no tiene un valor propio, y el contador de S1 se incrementa y se copia la dirección de S1 en S2 con lo cual se acelera la gestión de cadenas. Al leer S2 estamos leyendo de hecho S1. En cambio, si acto seguido hacemos :

S2 := S2 + '.';

Entonces el contador de S1 se reduce a 1, el contador de S2 se incrementa a 1 y se copia el contenido de S1 en la región reservada por S2 y se añade el punto. Todo este proceso es automático y es el compilador quien se encarga de añadir el código necesario para estas operaciones de forma que sea transparente para el programador.

Las cadenas largas son totalmente compatibles con las cadenas de tipo String[n] o ShortString. Si combinamos cadenas largas y cortas en una expresión, el resultado en general es AnsiString pero si lo asignamos a un ShortString se transforma automáticamente.

En el caso de los AnsiString no podemos saber el tamaño máximo de la cadena con High pues son cadenas dinámicas. Podemos especificar su tamaño máximo con SetLength.

SetLength(S1, 5); // Ahora S1 ha reservado espacio para 5 caracteres

Pero si hacemos una asignación a la cadena que exceda de su tamaño este no se trunca, como pasaría con los string Pascal, sino que crece adecuadamente.

program CadenasAnsi;

var

S1 : AnsiString;

begin

SetLength(S1, 3);

Writeln(Length(S1)); {3}

S1 := 'Hola que tal !';

Writeln(Length(S1)); {14}

end.

Los string Pascal aparte de estar limitados a 255 caracteres no son compatibles con las cadenas finalizadas en nulo que se emplean en C/C++ y por tanto tampoco lo son cuando llamamos funciones escritas en este lenguaje. Las cadenas AnsiString, a la contra, son más fáciles de convertir a cadenas C. 10.5.1.La directiva de compilador $H

La directiva de compilador $H permite determinar si el tipo String (sin indicador de longitud) representa a una cadena larga, AnsiString, o a una cadena corta, ShortString o String[255]. Cuando $H está activada entonces el tipo String representa un AnsiString. En caso contrario, representa un ShortString. La forma larga de la directiva $H es $LONGSTRINGS. Por defecto $H está activada. Esta directiva es local, de forma que se puede intercalar dentro del código tal como hacíamos con $I y sólo cambia si aparece una directiva $H posterior.

{$H+} // String = AnsiString

{$H-} // String = ShortString

{$LONGSTRINGS on} // String = AnsiString

{$LONGSTRINGS OFF} // String = ShortString

Cadenas finalizadas en carácter nulo

Algunos lenguajes de programación como C/C++ les falta un tipo especial para las cadenas de caracteres. Como solución se emplean arrays de caracteres sin índice que terminan cuando se encuentra un carácter nulo (de valor ASCII cero). Como hemos comentado esta estructura es incompatible con el tipo string de Pascal. Estas se basan en un byte inicial que indica el nombre de caracteres de la cadena (el valor que devuelve Length) y los bytes restantes son los caracteres de la cadena. El máximo valor que puede expresar el primer byte (de índice cero en la cadena) es 255 y por tanto las cadenas Pascal están limitadas a 255 caracteres como máximo.

Con este motivo existe el tipo PChar. Se puede entender como la equivalencia en Pascal de las cadenas terminadas en nulo. Formalmente el tipo PChar no es nada más que un puntero a un carácter. Esto es así ya que en las funciones y procedimientos en los que necesitemos pasar una cadena terminada en nulo no se pasa nunca la cadena en si, sólo el puntero al primer carácter de ésta. De esta forma la cadena puede ser muy larga pero la información pasada a la función sólo son los 4 bytes del puntero.

El asunto más complicado, por ahora, con las cadenas PChar es trabajar con ellas. Especialmente cuando hay que mezclarlas o convertir en cadenas AnsiString.

Un PChar lo podemos asignar directamente a un literal de cadena. Podemos asignar el contenido de un PChar a otro y a diferencia de los punteros habituales, la modificación de éste último no implica la modificación del primero.

program CadenesTerminadasNulo;

var

a, b : PChar;

begin

a := 'Hola';

b := a;

b := 'Adiós';

Writeln(a); { Hola }

Writeln(b); { Adiós }

end.

Muchas veces nos encontraremos en la tesitura de tener que pasar una cadena PChar a una función pero solo disponemos de un AnsiString o ShortString. En el caso de el AnsiString podemos hacer un typecasting directamente a PChar.

program CadenasTerminadasNulo;

procedure Escribir(Cadena : PChar);

begin

Writeln(Cadena);

end;

var

a : AnsiString;

begin

Escribir('Hola'); // Los literales son correctos

a := 'Hola';

Escribir(a); // Esto es ilegal

Escribir(PChar(a)); // Esto es perfectamente legal

end.

En el caso de un ShortString hay varias soluciones. La más simple es asignarla a un AnsiString y después aplicar el typecasting. Téngase en cuenta que en el caso de parámetros por referencia los typecastings no están permitidos y los parámetros tienen que ser del mismo tipo idéntico.

Para transformar una cadena PChar a su equivalente AnsiString o ShortString no hay que hacer ninguna operación especial. El compilador se encarga de transformar el PChar a String de forma automática.

El tipo array[0..X] of Char

Antes de poder emplear una variable PChar es importante inicializarla. Una forma poco elegante consiste en asignarla a un literal de cadena. Otra forma es emplear las complicadas rutinas de la unit Strings, que sólo operan con cadenas PChar, para obtener un puntero válido del tamaño deseado que posteriormente tendremos que liberar. Otra solución más simple son los arrays de caracteres indexados en cero que son totalmente compatibles con los PChar, se inicializan automáticamente y pueden tener el tamaño que queramos incluso más de 255 caracteres. El único caso en el que no serían compatibles son en los parámetros por referencia.

program CadenasTerminadasNulo;

procedure Escribir(Cadena : PChar);

begin

Writeln(Cadena);

end;

var

a : array [0..499] of Char; // El tamaño que queramos o necesitemos

begin

a := '1234'; // Hasta quinientos caracteres

Escribir(a);

end.

El tipo array of Char es compatible en asignación con ShortString y AnsiString.

Es un error bastante habitual declarar un array of Char demasiado corto lo que hace que los caracteres de más se trunquen. Este hecho puede provocar errores difíciles de detectar y por tanto es recomendable declarar arrays suficiente grandes para nuestros datos.

El tipo función/procedimiento

Es posible definir un tipo de dato que sea evaluable en una expresión, función, o llamado desde una instrucción mediante el que se llama tipo función, y extensivamente, tipo procedural.

El tipo procedural tiene utilidad en algunos casos muy concretos, por ejemplo en programación con DLL u otras librerías de enlace dinámico y por tanto veremos pocos ejemplos. Básicamente permite definir una variable de tipo de procedimiento o función que apuntará a otra función o procedimiento, una función de una DLL en memoria por ejemplo, y que se podrá tratar como si de un procedimiento o función se tratara. Vamos a ver un caso sencillo para una función. Para los procedimientos es muy parecido. Téngase en mente que vamos a emplear una función ya definida. Obsérvese también que es importante que los parámetros sean iguales, o al menos el numero de bytes de los parámetros idéntico, para evitar errores de desbordamiento de pila y de protección general. El ejemplo es bastante inútil pero es suficientemente ilustrativo.

Vamos a resolver una ecuación de segundo grado. Recordemos que las ecuaciones de segundo grado ax2 + bx + c = 0 se resuelven con la fórmula siguiente :

No se pudo entender (Falta el ejecutalbe de texvc; por favor, lee math/README para configurarlo.): x=frac{-bpmsqrt{b^2-4ac}}{4ac}

En caso de que el discriminante = b2 – 4ac sea menor que cero no habrá ninguna solución, en caso de que sea igual a cero tendremos una solución x = -b/2a y en caso de que sea positivo tendremos dos soluciones para los dos signos de la raíz cuadrada.

const

NO_SOLUCION = 0;

UNA_SOLUCION = 1;

DOS_SOLUCIONES = 2;

function ResolverEq2nGrado(a, b, c : real; var x1, x2 : real) : integer;

{ Resolución de ecuaciones de 2 grado en los reales

La función devuelve

NO_SOLUCION si no hay solución

UNA_SOLUCION si sólo hay una solución

DOS_SOLUCIONES si hay dos soluciones

}

var

discr : real; // El discriminante

begin

discr := b*b – 4*a*c;

if discr < 0 then

begin

ResolverEq2nGrado := NO_SOLUCION;

end

else

begin

if discr = 0 then

begin

ResolverEq2nGrado := UNA_SOLUCION;

x1 := -b / (2*a);

x1 := x2; // Dejamos constancia de q son iguales

end

else // Sólo puede ser discr > 0

begin

ResolverEq2nGrado := DOS_SOLUCIONES;

x1 := (-b + Sqrt( discr )) / (2*a);

x2 := (-b – Sqrt( discr )) / (2*a);

end;

end;

end;

Una vez ya tenemos definida la función vamos a ver como podemos llamarla mediante un tipo de función. Es importante definir el tipo de función con los mismos parámetros y especificando procedure o function según convenga.

type

TFuncion = function(a, b, c : real; var x1, x2 : real) : integer;

En este caso hemos copiado incluso el nombre de las variables, lo único que realmente importa es que tengan el mismo tamaño y mismo tipo de parámetro, por lo que la declaración siguiente es igual a la anterior :

type

TFuncion = function(A1, A2, A3 : real; var A4, A5 : real) : integer;

Una vez tenemos declarado el tipo podemos declarar una variable de este tipo.

var

Funcion : TFuncion;

Lo más interesante empieza ahora. Es necesario asignar la dirección de memoria de Funcion a la dirección de ResolverEq2nGrado. El código del programa queda de la forma siguiente :

program TipoFuncion;

const

NO_SOLUCION = 0;

UNA_SOLUCION = 1;

DOS_SOLUCIONES = 2;

function ResolverEq2nGrado(a, b, c : real; var x1, x2 : real) : integer;

begin

… // El código de la función está listado más arriba

end;

type

TFuncion = function(a, b, c : real; var x1, x2 : real) : integer;

var

Funcion : TFuncion;

x1, x2, a, b, c : Real;

begin

Write('A : '); Readln(a);

Write('B : '); Readln(b);

Write('C : '); Readln(c);

Funcion := @ResolverEq2nGrado; { Asignamos a Funcion la dirección de ResolverEq2nGrado }

case Funcion(a, b, c, x1, x2) of

NO_SOLUCION : begin

Writeln('Esta ecuación no tiene solución real');

end;

UNA_SOLUCION : begin

Writeln('Una solución : X=', x1:5:2);

end;

DOS_SOLUCIONES : begin

Writeln('Dos soluciones');

Writeln('X1=', x1:5:2);

Writeln('X2=' , x2:5:2);

end;

end;

end.

Obsérvese que hemos asignado a Funcion la dirección de ResolverEq2nGrado de forma que cuando llamamos a Funcion con los parámetros adecuados en realidad estamos llamando a ResolverEq2nGrado.

Para que los reales se vean con una cantidad adecuada de cifras y decimales emplearemos el añadido :5:2. Esta opción sólo está disponible en Writeln establece 5 cifras en total 2 de las cuales serán decimales del real. En el caso que el real tenga menos de 5 cifras, se escriben espacios en blanco hasta completarlas. Para los enteros también se puede emplear pero sólo se pueden especificar las cifras.

{ La constante Pi ya está declarada en Pascal }

Writeln(Pi:2:0); {2 cifras, 0 decimales}

Writeln(Pi:0:5); {0 cifras, 5 decimales}

Writeln(Pi:7:3);

Writeln((Pi*10):7:3);

Las instrucciones anteriores escribirían en pantalla :

83

3.14159

883.142

831.416

Dónde 8 representa un espacio en blanco. Obsérvese que el punto no se cuenta como una cifra decimal. Este valor que especifiquemos para cifras y decimales no tiene porque ser un literal, puede ser una variable entera o una constante entera.

Convenciones de llamada

No todos los lenguajes de programación llaman las funciones y procedimientos de la misma forma. Por ejemplo, el compilador Borland Delphi por defecto pasa los parámetros de izquierda a derecha mientras que FreePascal y los compiladores de C los pasan de derecha a izquierda. En función de quien se encarga de liberar la pila de la función y en el orden en el que se pasan los parámetros tendremos una convención de llamada u otra.

Para llamar una función que se encuentre en memoria es conveniente emplear la convención adecuada. Básicamente las convenciones más habituales son cdecl y stdcall, esta última es la convención por defecto de FreePascal.

Convención de llamada

Paso de parámetros

Liberación de memoria

Parámetros en los registros ?

por defecto

Derecha a izquierda

Función

No

cdecl

Derecha a izquierda

Quien hace la llamada

No

export

Derecha a izquierda

Quien hace la llamada

No

stdcall

Derecha a izquierda

Función

No

popstack

Derecha a izquierda

Quien hace la llamada

No

register^4 <#sdfootnote4sym>

Izquierda a derecha

Función

pascal

Izquierda a derecha

Función

No

 

Convenciones export, cdecl i popstack

Se emplean en funciones que tendrán que ser llamadas por código en C o que han sido escritas con la convención de llamada de C/C++. Popstack además nombra la rutina con el nombre que el compilador FreePascal le daría si fuera una función normal.

Convenciones pascal i register.

La convención register es ignorada por el compilador que la sustituye por la convención stdcall. La convención pascal tiene una mera finalidad de compatibilidad hacia atrás.

Convención stdcall

La convención stdcall es la convención de llamada por defecto de FreePascal. Además, es la convención de llamada de las funciones de la API de Win32.

Para indicar al compilador qué convención de llamada tiene que emplear sólo hay que especificar la convención de llamada después de la declaración de la función. Téngase en cuenta de que las convenciones de llamada son mutuamente excluyentes entre si, por lo que, no podemos definir más de una. Obsérvese el ejemplo siguiente :

procedure LlamadaStdCall; stdcall; // Convención stdcall

begin

end;

procedure LlamadaCdeCl; cdecl; // Convención cdecl

begin

end;

Vamos a ver un ejemplo práctico de las diferencias entre convenciones a la hora de pasar parámetros. Emplearemos la convención pascal y la convención stdcall, por defecto, para pasar cuatro enteros de 4 bytes sin signo, Cardinal. Después veremos que es posible acceder a ellos y obtener su dirección en memoria.

program ConvencionLlamada;

procedure IzquierdaDerecha(a, b, c, d : Cardinal); pascal;

var

Puntero : ^Cardinal;

begin

Puntero := @a;

Writeln(Puntero^, ' (', Cardinal(Puntero), ')' ); {a}

Dec(puntero);

Writeln(Puntero^, ' (', Cardinal(Puntero), ')'); {b}

Dec(puntero);

Writeln(Puntero^, ' (', Cardinal(Puntero), ')'); {c}

Dec(puntero);

Writeln(Puntero^, ' (', Cardinal(Puntero), ')'); {d}

end;

procedure DerechaIzquierda(a, b, c, d : Cardinal); stdcall;

var

Puntero : ^Cardinal;

begin

Puntero := @a;

Writeln(Puntero^, ' (', Cardinal(Puntero), ')'); {a}

Inc(puntero);

Writeln(Puntero^, ' (', Cardinal(Puntero), ')'); {b}

Inc(puntero);

Writeln(Puntero^, ' (', Cardinal(Puntero), ')'); {c}

Inc(puntero);

Writeln(Puntero^, ' (', Cardinal(Puntero), ')'); {d}

end;

begin

Writeln('Izquierda a derecha :');

IzquierdaDerecha(1, 2, 3, 4);

Writeln;

Writeln('Derecha a izquierda :');

IzquierdaDerecha (5, 6, 7, 8);

end.

Cuando ejecute el programa obtendrá un resultado parecido al siguiente :

Izquierda a derecha :

1 (39321120)

2 (39321116)

3 (39321112)

4 (39321108)

Derecha a izquierda :

5 (39321108)

6 (39321112)

7 (39321116)

8 (39321120)

El programa nos muestra el valor de las variables que hemos pasado a las dos funciones y entre paréntesis nos indica la dirección en memoria del parámetro en cuestión. Es legal un typecasting de puntero a Cardinal pues un puntero no es nada más que un entero de cuatro bytes para acceder a una posición de memoria. En el código obtenemos la dirección del parámetro a y después obtenemos los otros parámetros mediante aritmética de punteros. Recuerde que Inc y Dec incrementan o decrementan el puntero en tantos bytes como el tamaño del tipo de puntero. Un cardinal tiene un tamaño de cuatro bytes por tanto el puntero apuntará cuatro bytes más adelante o atrás después de un Inc o Dec, respectivamente.

Los parámetros se pasan, en el primer caso, de izquierda a derecha, o sea, del último al primero. De forma que el último parámetro d es el que se encuentra en las posiciones más bajas de la memoria y los parámetros van pasando del último al primero, a, el cual tendrá la posición más a la derecha, o sea la posición de memoria más alta.

En el segundo caso los parámetros se pasan de derecha a izquierda, o sea, del primero al último. En este caso, el último parámetro d está en la posición de memoria más alta mientras que el primer parámetro a está en la posición más a la derecha, o sea, en la posición más baja de la memoria.

Izquierda a derecha

Posiciones de memoria

39321108

39321112

39321116

39321120

d

c

b

a

Derecha a izquierda*

a

b

c

d

Es importante emplear la convención adecuada cada vez que llamamos a una función. Generalmente no tendremos que preocuparnos pues el compilador sabe qué convención se está empleando en las funciones o procedimientos que declaremos en el código. En el caso de que empleemos funciones definidas en librerías escritas en otros lenguajes de programación habrá que tener mucho cuidado y especificar la convención adecuada, en otro caso emplearía la convención stdcall por defecto. También es posible especificar una convención a un tipo procedural :

type

TIzquierdaDerecha = procedure (a, b, c, d : Cardinal); pascal;

Obtenido de ""

Manual de FreePascal (parte final)

De Gleducar, http://www.gleducar.org.ar

Units

La necesidad de las units

Hasta ahora hemos empleado algunas funciones que se encontraban en algunas units estándar de FreePascal. Las units permiten reunir funciones, procedimientos, variables y tipos de datos para disponerlos de forma más ordenada y poderlos reutilizar en otras aplicaciones.

Una primera forma de reutilizar el código que hemos visto ha sido mediante la declaración de funciones. Estas funciones tenían que estar declaradas antes de que las hiciéramos servir. Habitualmente, pero, emplearemos funciones y procedimientos en varios programas y en otras units y no será práctico copiar de nuevo la implementación. Además, de esta forma tendríamos que cambiar muchas veces el código si lo optimizáramos o lo corrigiéramos, por ejemplo. Empleando units conseguimos reutilizar mejor las funciones, procedimientos pues estas se encontrarán en la unit y cualquier cambio que necesitemos sólo se tendrá que realizar en la unit misma. Cuando necesitemos llamar las funciones y los procedimientos de la unit, o emplear variables de ésta, sólo habrá que indicar al compilador que las emplee.

Utilizar una unit

Para emplear una unit en nuestro programa ya hemos visto que es tan simple como especificarla en la cláusula uses del programa. Normalmente necesitaremos más de una, pues las separamos entre comas.

uses Crt, Dos, SysUtils;

Hay que tener en cuenta de que la unit tendrá que ser visible para el compilador. Normalmente el compilador buscará el archivo que contenga la unit en el directorio del programa más algunos directorios del compilador. También podemos especificar la posición en el disco de la unit, en caso de que no se encontrara en el directorio actual, con el parámetro -Fudirectorio pasado al compilador, donde directorio es el directorio donde el compilador puede encontrar la unit. Otra forma es modificar el archivo PPC386.CFG al directorio C:PPBINWIN32 y añadir la línea -Fudirectorio. Si tenemos que especificar más de un directorio pues añadiremos más líneas. De esta forma es permanente cada vez que se ejecuta el compilador. Finalmente podemos emplear la directiva de compilador, dentro del código del programa o unit, $UNITPATH separando los directorios con puntos y comas. :

{$UNITPATH directorio1;..;directorion}

Por ejemplo:

{$UNITPATH ..graficos;c:units}

Crear una unit

Para crear una unit hay que tener en cuenta que el archivo que vamos a crear, con extensión PAS o PP tiene que tener el mismo nombre que le daremos a la unit. Por ejemplo si nuestra unit se llama Utilidades (recuerde que las mayúsculas son poco importantes en Pascal) el archivo tendrá que ser UTILIDADES.PAS o bien UTILIDADES.PP. Es muy recomendable poner el nombre del archivo de la unit en minúsculas sobretodo en el caso de Linux donde los archivos son sensibles a mayúsculas y minúsculas.

El encabezado de una unit tiene que empezar con la palabra reservada unit y el nombre de la unit, que tiene que coincidir con el nombre del archivo. En nuestro caso sería :

unit Utilidades;

Esta palabra reservada indica al compilador que vamos a hacer una unit y no un programa. Además, a diferencia de program, que era opcional, unit es necesaria para indicarle al compilador que lo que se va a encontrar es una unit y no un programa, como suele ser por defecto.

Estructura de una unit

Una unit está formada por tres partes bien diferenciadas : una interfaz, una implementación y una sección de inicialización.

En la interfaz se declaran todas las variables, constantes, tipos y funciones o procedimientos que queremos que estén disponibles cuando vayamos a emplear la unit. En lo que los procedimientos y funciones concierne, los declararemos como si se trataran de funciones forward pero sin la palabra reservada forward.

En la implementación implementaremos las funciones de la interfaz y podemos declarar variables, tipos, constantes y otras funciones. A diferencia de los declarados en la interfaz, los elementos declarados en la implementación sólo están disponibles dentro de la misma unit y tienen como finalidad ser elementos auxiliares en la implementación.

Finalmente el código de inicialización se ejecutará al cargase la unit. Este código se ejecuta antes que se inicie el programa y tiene como finalidad realizar tareas de inicialización de la propia unit. Como es de esperar, la unit termina con un end.

La parte interface

La interfaz se implementa primero y se indica su inicio con la palabra reservada interface. Justo después de interface podemos especificar las units que empleará la función aunque sólo es recomendable hacerlo si alguno de los tipos de datos que vamos a emplear en interface se encuentra en otra unit. Si sólo la necesitamos para ciertas funciones ya la incluiremos dentro de la implementación. Esto es así para evitar referencias circulares donde A emplea B y B emplea A, ya que el compilador emitirá un error. En la sección implementación este error no se puede dar.

Vamos a implementar diversas funciones y procedimientos simples en nuestra unit que nos faciliten un poco más el trabajo a la hora de pedir datos al usuario. Concretamente vamos a hacer diversas funciones sobrecargadas que emitirán un mensaje y comprobarán que el dato introducido es correcto. La unit empieza de la forma siguiente :

unit Utilidades;

interface

procedure PedirDato(Cadena : String; var Dato : String);

procedure PedirDato(Cadena : String; var Dato : Integer);

procedure PedirDato(Cadena : String; var Dato : Real);

Sólo lo implementaremos para estos tres tipos pues no se suelen pedir otros tipos de datos ni caracteres sueltos (en este caso pediríamos la pulsación de una tecla). Ahora sólo falta implementar las funciones en la sección de implementación.

La parte implementation

Una vez declarados en interface tipos de datos, constantes y variables no hay que hacer nada más. No es así con las funciones y los procedimientos que hay que implementar. La sección de implementación empieza con la palabra reservada implementation. No importa mucho el orden en el que las implementemos sino que realmente las implementemos, de no hacerlo el compilador nos dará error.

implementation

procedure PedirDato(Cadena : string; var Dada : string);

begin

Write(Cadena); Readln(Dato);

end;

procedure PedirDato(Cadena : string; var Dato : Integer);

begin

Write(Cadena); Readln(Dato);

end;

procedure PedirDato(Cadena : string; var Dato : Real);

begin

Write(Cadena); Readln(Dato);

end;

end. // Fin de la unit

Curiosamente las implementaciones de las tres funciones son idénticas pero como ya hemos comentado no tiene porque ser así. Por ejemplo podíamos haber pedido un string al usuario y haber añadido código de comprobación para ver si es un entero o no, devolviendo true o false en función de si la función ha tenido éxito. Esto es así ya que Readln falla si al pedir un entero, o un real, el usuario introduce un dato que no es convertible a entero o real.

Inicialización de una unit

Supongamos que tenemos una unit que dispone de una orden llamada NotificarLog(Cadena : String); que se encarga de escribir en un archivo de logging (donde se anotará lo que va haciendo el programa y así si falla saber dónde ha fallado) la cadena que se especifica como parámetro. En este caso nos interesa abrir un archivo de texto para hacer las anotaciones y cerrarlo si el programa termina ya sea de forma normal o anormal. Es por este motivo que las units incorporan una sección de inicialización y también de finalización. En la especificación habitual de Pascal sólo había sección de inicialización pero recientemente a Delphi se le añadió la posibilidad de disponer también de una sección de finalización y FreePascal también la incorpora.

Inicialización "a la antigua"

Es posible inicializar, sólo inicializar, si antes del end. final añadimos un begin y en medio las sentencias de inicialización. Por ejemplo podríamos haber puesto unos créditos que se verían al inicializarse la unit.

begin

Writeln('Unit de utilidades 1.0 – Roger Ferrer Ibáñez');

end. // Fin de la unit

Este tipo de inicialización tiene la desventaja de que no permite la finalización de la unit de forma sencilla. Sólo nos será útil en algunos casos. Cuando necesitemos inicializar y finalizar tendremos que emplear otra sintaxis.

Inicialización y finalización

Debido a que la mayoría de inicializaciones requerirán una finalización es posible añadir secciones de inicialización y finalización en la parte final de la unit. Estas secciones irán precedidas por las palabras reservadas initialization y finalization. En este caso especial no es necesario rodear las sentencias de begin ni de end. Tampoco es obligatoria la presencia de los dos bloques : puede aparecer uno sólo o bien los dos a la vez.

Ahora ya podemos implementar nuestra función de logging.

unit Logging;

interface

procedure NotificarLog(Cadena : string);

implementation

var // Variable de la unit no accesible fuera de la unit

Log : Text;

procedure NotificarLog(Cadena : string);

begin

Writeln(Log, Cadena);

end;

initialization

Assign(Log, 'LOGGING.LOG');

Rewrite(Log);

finalization

Close(Log);

end. // Fin de la unit

Orden de inicialización y finalización

El orden de inicialización y finalización de las units depende exclusivamente de su posición en la cláusula uses. El orden es estrictamente el que aparece en esta cláusula. Supongamos que tenemos la cláusula siguiente en nuestro programa (o unit) :

uses Unit1, Unit2, Unit3;

Supongamos que todas tienen código de inicio y final. Entonces la Unit1 se inicializaría primera, después Unit2 y después Unit3.

El proceso de finalización es a la inversa. Primero se finalizaría la Unit3, después Unit2 y después Unit1. En general el proceso de inicio es simétrico : las units que se han inicializado primero también son las últimas a finalizarse.

En el caso de que una unit que empleamos necesite otra, entonces el criterio del compilador es inicializar todas las units que se encuentre más afuera primero y después las de más adentro. Por ejemplo, si en uses sólo hubiera Unit1 pero ésta empleara Unit2 y ésta última Unit3 entonces primero se inicializaría Unit3, después Unit2 y finalmente Unit1. El proceso de finalización seria el inverso, tal como se ha comentado.

A diferencia de C y de algunos lenguajes, incluir dos veces una misma unit en el código (por ejemplo que Unit1 y Unit2 empleen Unit3) no comporta ningún problema y la inicialización y finalización sólo se lleva a cabo una vez siguiendo el orden antes indicado.

ÿmbito de una unit

Se llama ámbito a aquel conjunto de partes del código que tienen acceso a un identificador, ya sea una constante, variable, tipo o función/procedimiento.

Interface

Si el identificador se declara en la sección interface entonces el ámbito de este será :

  • Toda la propia unit del identificador incluyendo las secciones interface, implementation, initialization y finalization.
  • Todas las units y programas que tengan esta unit dentro de la cláusula uses.

Implementation

Si el identificador se declara en la sección implementation entonces su ámbito es :

  • Toda la propia unit a excepción de la sección interface. Esto quiere decir, por ejemplo, que no podremos emplear tipos de datos declarados en implementation dentro de los parámetros de una función en interface, puesto que no sería posible para el programador pasar parámetros a esta función fuera de la propia unit pues no conocería el tipo de dato.
  • Fuera de la unit estos identificadores no son accesibles.

Incluir units dentro de interface

Es posible especificar una cláusula uses dentro de la sección interface. Esta posibilidad sólo se tendría que emplear en caso de que algún identificador declarado en interface precise de algún otro tipo declarado en otra unit.

Situando la cláusula uses en interface permitimos que toda la unit, incluidas implementation, initialization y finalization, acceda a esta otra unit.

Ahora bien, esta posibilidad tiene el riesgo de que nuestra unit también se encuentre en la sección interface de la otra unit lo que resultaría en una referencia circular que es ilegal.

Incluir units dentro de implementation

En este caso sólo las secciones implementation, initialization, finalization tienen acceso a los identificadores declarados en la unit en uses. Es posible que dos units se incluyan mutuamente siempre que no se incluyan en interface a la vez, ya sea las dos en implementation o una de ellas en interface y la otra en implementation. Siempre que sea posible, es recomendable incluir las units en la cláusula de implementation.

[editar]

Librerías

Librerías vs units

Hasta ahora hemos visto que todos los elementos que conformaban nuestro programa se resolvían en tiempo de compilación. Es lo que se llama enlace estático. Una vez se ha compilado el programa, el propio EXE dispone de todo lo necesario para ejecutarse.

En el caso de las units escritas compiladas con FreePascal, la compilación nos dará un archivo PPW que contiene la información que en tiempo de compilación se enlazará adecuadamente al EXE final.

Algunas veces, pero, el código no habrá sido generado por nosotros sino que con otros compiladores. Para poder emplear este código dentro de nuestro programa tendremos que enlazarlo. En algunos casos realizaremos enlaces estáticos, resueltos en tiempo de compilación, pero en otros tendremos que enlazar en tiempo de ejecución, especialmente en el caso de las librerías de enlace dinámico (DLL, Dynamic Library Linking) de Windows.

Archivos objeto

FreePascal permite el enlace estático con archivos llamados archivos objeto (que suelen llevar extensión O, o OW en Win32) que hayan sido compilados con los compiladores GNU Pascal, GNU C/C++ o algún otro compilador que compile este formato de archivos objeto. Los compiladores Borland, Microsoft e Intel generan un tipo de archivos objeto que en general no son directamente compatibles con los archivos objeto que vamos a emplear.

Importación de rutinas en archivos objeto

Supongamos que queremos enlazar una función muy sencilla escrita en C que tiene el código siguiente :

int incrementador(int a)

{

return a+1;

}

Esta función toma como parámetro un entero de 32 bits (el parámetro a) y devuelve otro entero que es el valor del parámetro incrementado en uno. En Pascal esta función sería la siguiente :

function incrementador(a : integer) : integer;

begin

incrementador := a+1;

end;

Como se puede ver C y Pascal tienen un cierto parecido sintáctico. Supongamos que la rutina en C se encuentra almacenada en un archivo llamado incr.c. Ahora hay que compilarlo para obtener un archivo objeto. Como que no es un programa completo, pues no tiene bloque principal, indicaremos al compilador que sólo lo compile y no llame al enlazador, que nos daría un error. El compilador de C que emplearemos en los ejemplos en C es el conocido GNU CC, o gcc. Para compilar nuestro código ejecutamos el compilador desde la línea de órdenes habitual:

Partes: 1, 2, 3, 4, 5, 6
 Página anterior Volver al principio del trabajoPágina siguiente