Monografias.com > Sin categoría
Descargar Imprimir Comentar Ver trabajos relacionados

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




Enviado por rafaelfreites



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

gcc -c -o incr.o incr.c

El parámetro -c indica que sólo queremos
compilación y no enlazado. El parámetro -o seguido
de incr.o sirve para indicar que el archivo objeto
que se creará llevará el nombre de incr.o.
Finalmente incr.c es el archivo que estamos compilando.

Una vez compilada la rutina en un archivo objeto ahora hay que
enlazarla en nuestro programa Pascal. Para
hacerlo tendremos que realizar varios pasos.

Primero hay que indicar al compilador que habrá que
enlazarse con el archivo objeto incr.o. Para hacerlo emplearemos
la directiva de compilador $LINK seguida del nombre de archivo
objeto a incluir. Aunque no es necesario incluir la
extensión .o (que es la extensión por defecto) es
recomendable.

{$LINK incr.o}

Una vez hemos incluido esta directiva en el principio del
programa ahora habrá que declarar una función
externa. Las funciones
externas se declaran con la directiva external que indica al
compilador que esta función se encuentra en un
módulo externo al programa y que no la estamos
implementando nosotros. En nuestro caso, además, tendremos
que añadir la directiva cdecl pues esta rutina está
escrita en C y conviene que el compilador pase los
parámetros correctamente y además encuentre el
nombre de la función dentro del archivo objeto. El archivo
objeto no contiene los nombres de las funciones directamente sino
que éste sufre modificaciones llamadas mangling. La
directiva cdecl indica a FreePascal que busque el mangled name
apropiado en C de nuestra función en el archivo
objeto.

También hay que tener en cuenta que en este caso la
declaración de la función tendrá en cuenta
las mayúsculas y las minúsculas. En nuestro caso la
función incrementador en C estaba toda en
minúsculas y en el código
Pascal vamos a tener que hacer lo mismo. Finalmente hay que
emplear el mismo tipo de parámetro. En Pascal el tipo int
de C es equivalente a Longint.

function incrementador(a : longint) : longint;
cdecl; external;

Tal como hemos indicado, las siguientes posibles declaraciones
serían sintácticamente correctas pero el enlazador
no sabría resolver a qué función estamos
llamando.

function INCREMENTADOR(a : longint) : longint;
cdecl; external;

function incremENTADOR(a : longint) : longint;
cdecl; external;

Una vez la función ha sido declarada ya la podemos
emplear dentro de nuestro código ahora ya sin
restricción de mayúsculas. Un programa de ejemplo
de nuestro código sería el siguiente.

program FuncionC;

{$L INCR.O}

function incrementador(a : longint) : longint;
cdecl; external;

var

numero : longint;

begin

write('Introduzca un número : ');
Readln(numero);

Write('Número + 1 : ', Incrementador(numero));

end.

En realidad la mayoría de llamadas a funciones externas
suelen ser más complejas pues pueden incorporar variables por
referencia. En el ejemplo siguiente emplearemos un procedimiento que
incrementa la variable que le pasemos. El código en C es
el siguiente:

void incrementador(int* a)

{

*a = *a+1;

}

En realidad lo que estamos pasando es un puntero tal como
indica el operador de desreferenciación de C *, similar al
operador ^ de Pascal. Lo que hacemos es incrementar el contenido
del puntero en una unidad. Esto es así porque C no
incorpora parámetros por referencia pero permite pasar
punteros como parámetros. La directiva void indica que
esta función no devuelve nada por lo que es un
procedimiento. Una vez compilado y obtenido el archivo objeto
nuestro programa puede quedar así:

program FuncionC;

{$L INCR.O}

procedure incrementador(var a : Longint); cdecl;
external;

var

numero : Longint;

begin

Write('Introduzca un número : ');
Readln(numero);

Incrementador(numero);

Writeln('Número + 1 : ', numero);

end.

En realidad cuando pasamos un parámetro por referencia
lo que estamos pasando es su puntero por tanto esta
declaración así es del todo correcta.
Obsérvese que en este caso hemos declarado un
procedimiento y no una función ya que, como sabemos,
nuestro código no devuelve nada.

Aunque C no incorpora parámetros por referencia, C++
sí. Vamos a sustituir el código incr.c por un
código en C++ que reciba los parámetros por
referencia.

*void* incrementador(int &a)

{ a = a+1;

}

El operador & de C++ indica que el parámetro es por
referencia. Como podemos ver ahora ya no hay que emplear el
operador * ya que el parámetro se trata como una variable
normal. Para compilar el código en C++ hay que emplear el
compilador g++ en vez de gcc con los mismos parámetros que
hemos empleado antes. Una vez obtenido el archivo objeto si
intentamos compilar el archivo obtendremos un error. Esto es
debido a que el mangling de C++ es muy distinto al de C. Por
tanto tendremos que indicarle al compilador de C++
explícitamente que haga mangling al estilo de C. Para
hacerlo habrá que modificar ligeramente la
función :

*extern* "C" void incrementador(int &a)

{ a = a+1;

}

Volvemos a compilar el código C++ para obtener el
archivo objeto. Ahora al compilar el programa en Pascal todo
funciona correctamente. En realidad si hubiéramos
compilado con el mangling de C++ tendríamos que haber
llamado la función __incrementador_FRi.

Podemos indicar al compilador cual es el nombre real de la
función que estamos importando. Para hacerlo sólo
hay que añadir la directiva name después de
external y el nombre, sensible a mayúsculas, de la
función. Por ejemplo si compilamos el primer ejemplo de
código en C++ tendremos que llamar la función por
el nombre _incrementador__FRi. La declaración de la
función quedaría de la forma siguiente :

procedure Incrementador(var a : Longint); cdecl; external
name '_incrementador__FRi';

En este caso el nombre que demos a la función no se
tiene en cuenta ya que se tiene en cuenta el valor que
indica name. Podíamos haberla declarado como INCREMENTADOR
y todo hubiera funcionado igual. Este método lo
podemos emplear siempre para ignorar el mangling por defecto de
la convención de llamada.

Exportación de funciones en
archivos
objeto

Cuando compilamos un archivo, ya sea una unit o un
programa, con FreePascal siempre se obtiene un archivo objeto.
Este archivo contiene las declaraciones del programa, variables,
etc una vez se han compilado. Para poder exportar
funciones en este archivo objeto tendremos que emplear una unit,
ya que los programas definen
símbolos que nos pueden conllevar problemas a la
hora de enlazar con otros programas.

Las funciones que queramos exportar tendrán que
llevar la directiva export para indicar al compilador que esta
función va a ser exportada, en caso contrario no se
podría acceder a ella. Para evitar problemas con el
mangling de las funciones es recomendable declarar las funciones
que exportemos como cdecl o bien definir un nombre o alias para
la función :

unit PruebaObj;

interface

implementation

procedure incrementar(var a : integer); export;
stdcall; [Alias : 'increm' ];

begin

a := a + 1;

end;

end.

Cuando referenciemos a esta función desde
código C, por ejemplo, podremos emplear el nombre increm,
sensible a mayúsculas, que hemos declarado en la estructura del
alias.

Hay que tener en cuenta de que las funciones y procedimientos
exportados no deberían ser llamados por otras funciones
puesto que emplean un mecanismo diferente para el paso de
parámetros. Aunque es posible llamar funciones no
exportadas dentro de funciones exportadas sin ningún
problema. Para impedir que el programador llame a estas funciones
se omite la declaración en interface y sólo se
implementa dentro del bloque implementation. De esta forma no es
posible llamar a la función, aun cuando esta sea incluida
en un programa o unit.

Para llamar a una función exportada hay que
emplear el mecanismo que hemos visto antes para importar
funciones de archivos de objeto. Cada lenguaje de
programación tiene sus mecanismos para importar
archivos objeto, si es que tiene.

Hay que tener en cuenta que si una función
exportada llama funciones que se encuentran en otras units
también necesitaremos sus archivos objeto. Por ejemplo la
función Writeln se encuentra definida en la unit System y
necesitaremos el archivo System.o (o bien, System.ow). Esta unit
no es necesario añadirla en la cláusula uses ya que
se añade siempre automáticamente por el
compilador.

Hay otras restricciones por lo que a las funciones
exportadas concierne. Por ejemplo los ShortString (ni String[n])
no se exportan correctamente y es recomendable emplear PChar o
bien, si es para emplearse en programas y units compilados con
FreePascal, el tipo AnsiString que sí que se exporta
correctamente. Finalmente hay que tener en cuenta de que los
nombres de los tipos de datos,
como los enteros que hemos visto antes, pueden variar de un
lenguaje a
otro.

Exportar e importar variables

De la misma forma que podemos importar y exportar
funciones y procedimientos también podemos exportar e
importar variables. Para exportar una variable emplearemos de
nuevo una unit. A diferencia del caso anterior no hay problema en
acceder a ella por lo que podremos incluirla en la sección
interface de una unit sin ningún problema.

Es muy recomendable que el nombre de la variable se
guarde en el archivo objeto con mangling de C. Para hacerlo
añadiremos la directiva cvar después de la
declaración de una única variable. No es posible
emplear un mismo cvar para varias variables pero sí varios
cvar para cada variable declarada. En este caso la
declaración será sensible a las mayúsculas
aunque en el código en Pascal podremos referenciarla como
queramos. La unit que emplearemos de ejemplo es la
siguiente :

unit ExportarVariable;

interface

implementation

var valor : Longint; cvar;

procedure Cuadrado(a : Longint); *cdecl*; *export*;
// Exportamos la función

begin

Valor := a*a;

end;

end.

Para importar una variable tenemos dos formas. La
primera se basa en aprovecharse del método de mangling de
C. Para hacerlo habrá que añadir la directiva cvar
seguida de la directiva external, que indica al compilador que no
reserve memoria para esta
variable pues es externa. En este caso el nombre de
declaración es sensible a las mayúsculas aunque
desde el código podremos referirnos a ella sin
distinción de mayúsculas.

La otra forma de declarar una variable externa permite
emplear el identificador que queramos pero implica conocer el
mangled name de la variable. En C las variables llevan un
guión bajo delante del identificador. De esta forma
podemos declarar la variable externa de nuestro archivo
objeto.

{$LINK EXPORTARVARIABLE.OW}

var

valor: Longint; *cvar*; *external*; // Valor, VALOR u
otros no son válidos

alias : Longint; *external* name
'_valor';

Como se observa, las variables valor y alias se
referirán a la misma variable de forma que cualquier
cambio en esta
se apreciará en el valor de las dos variables y viceversa,
las asignaciones a cualquiera de ellas dos afectarán al
valor de la otra. Finalmente vamos a importar la función
Cuadrado que hemos exportado en la unit.

procedure Cuadrado(a : Longint); *cdecl*;
*external*; // quadrat, o QUADRAT no valen

El programa completo es el siguiente :

program ImportarFuncionesyVariables;

{$L EXPORTARVARIABLE.OW}

procedure Cuadrado(a : Longint); *cdecl*;
*external*;

var

valor : Longint; *cvar*; *external*;

alias : Longint; *external* name
'_valor';

begin

Cuadrado(6);

Writeln(valor, ' = ', alias);

end.

Librerías de enlace
dinámico

Hasta ahora hemos visto mecanismos de enlace
estático con funciones y procedimientos y variables. La
existencia de la función y su posterior enlace con el
código se ha resuelto todo el rato antes de ejecutar el
programa, en tiempo de
compilación.

Las librerías de enlace dinámico (con
extensión DLL en Windows y so
en Linux) permiten
realizar el enlace en tiempo de ejecución.

El enlace dinámico tiene varias ventajas. El
archivo, al no ir enlazado estáticamente, puede ser
sustituido por otro que corrija errores o mejoras en el algoritmo,
siempre que se conserven los nombres y los parámetros de
las funciones. De esta forma se puede actualizar la
librería. En el caso del enlace estático
habríamos tenido que recompilar el programa para
actualizarlo. De esta forma, el programador la mayoría de
veces sólo tiene que preocuparse en cuánto y
cómo llamar a la función más que no como ha
sido implementada, pues los cambios en la implementación
no implican recompilar el programa de nuevo.

Creación de una librería
de enlace dinámico

La creación de una librería de enlace
dinámico es parecida a un programa. Para empezar es
necesario que la librería empiece con la palabra reservada
library que indica al compilador que lo que se encontrará
responde a la forma de una librería. Hay que
acompañar a la palabra reservada de un identificador que
no es necesario que tenga el mismo nombre que el
archivo.

Las funciones y procedimientos se declararán de
la forma habitual, sin export, y especificando la
convención de llamada. Es muy habitual en Windows la
convención stdcall para funciones en DLL por lo que se
recomienda esta convención.

Para exportar las funciones emplearemos la
cláusula exports (con ese final, es importante) seguida de
los identificadores de función o procedimiento que
queramos exportar y, opcionalmente pero más que
recomendable, el nombre de exportación de la función. Este
será el nombre con el que llamaremos después la
función o procedimiento de la librería. En caso
contrario, si no lo especificamos se empleará el mangling
por defecto de FreePascal, que en el caso concreto de
las DLL es todo el nombre de la función toda en
mayúsculas.

library CuadradosyCubos;

function Cuadrado(a : longint) : longint;
*stdcall*;

begin

Cuadrado := a*a;

end;

function Cubo(a : longint) : longint;
*stdcall*;

begin

Cubo := a**3;

end;

exports

Cuadrado name 'Cuadrado',

Cubo name 'Cubo';

end.
En este ejemplo estamos exportando dos funciones Cuadrado y Cubo.
Una vez compilada la librería obtendremos un archivo .DLL
que ya podrá ser llamado desde nuestro
programa.

Las funciones que exporte la librería no tienen
porqué estar declaradas forzosamente dentro de la
librería. Pueden estar declaradas en la interfaz de una
unit que se encuentre dentro de la cláusula uses
después de la cabecera library.

Importación de funciones en
librerías de enlace dinámico

La forma de importar las funciones y los procedimientos
es parecida. Ahora no es necesario especificar ninguna directiva
como $LINK y el compilador tampoco detectará si la
función es incorrecta. Por este motivo es importante
especificar los parámetros correctamente conjuntamente con
la convención de llamada adecuada.

En este caso la directiva external tiene que ir seguida
del nombre de la librería y puede ir seguida de una
directiva name que especifique el nombre. En caso contrario se
empleará el mangling habitual.

program FuncionesDLL;

const

NOMBREDLL = 'CuadradosyCubos';

function Cubo(a : longint) : longint;
*stdcall*; *external* NOMBREDLL name 'Cubo';

function Cuadrado(a : longint) : longint;
*stdcall*; *external* NOMBREDLL name 'Cuadrado';

var

a : integer;

begin

Write('Introduzca un número : ');
Readln(a);

Writeln('Cuadrado : ', Cuadrado(a), ' Cubo :
', Cubo(a));

end.

Como se puede ver, es posible y muy recomendable,
emplear constantes cuando nos referimos a una misma
librería a fin de evitar errores tipográficos. En
este ejemplo hemos supuesto que la librería era
CUADRADOSYCUBOS.DLL. También podemos observar que no
importa el orden en el que se importan las funciones. En este
caso no hemos especificado la extensión en NOMBREDLL, que
por defecto en Win32 es .DLL, pero es posible especificarla sin
ningún problema.

Este programa sólo funcionará si es capaz
de encontrar la librería CUADRADOSYCUBOS.DLL en alguno de
estos directorios :

  • El directorio donde se encuentra el archivo
    ejecutable.
  • El directorio actual del sistema de
    archivos.
  • El directorio de Windows. Habitualmente
    C:WINDOWS
  • El subdirectorio SYSTEM de Windows. Habitualmente
    C:WINDOWSSYSTEM
  • Los directorios de la variable PATH.

En caso de no existir o que no se encontrará la
librería en alguno de estos directorios, Windows nos
mostraría un error y no podríamos ejecutar el
programa. Igualmente pasaría si la función que
importemos de una DLL no existiera. En este aspecto es importante
remarcar el uso de la directiva name para importar correctamente
las funciones.

Importación y exportación
de variables en DLL

Es posible en FreePascal, y Delphi,
exportar variables en librerías DLL aunque sólo el
primero permite importarlas mediante la sintaxis. El
método para hacerlo es muy similar a exportar e importar
funciones. Por ejemplo, la librería siguiente exporta una
función que inicializará la variable
exportada.

library VarExport;

var

variable_exportada : integer;

procedure Inicializar;

begin

variable_exportada := 10;

end;

exports

Inicializar name 'Inicializar',

variable_exportada name 'variable_exportada';

end.

El programa siguiente importa la función y la
variable. Supongamos que la DLL recibe el nombre
VarExport.DLL.

program ImportarVar;

const

VarExportDLL = 'VarExport';

var

variable : integer; external VarExportDLL name
'variable_exportada';

procedure Inicializar; external VarExportDLL name
'Inicializar';

begin

Inicializar;

Writeln(variable); {10}

end.

Importación dinámica de funciones en librerías
DLL (sólo Win32)

Mediante la API de Win32, interfaz de programación de aplicaciones, podemos
importar funciones de librerías de forma
programática. De esta forma podemos controlar si la
función existe o si la librería se encuentra en el
sistema y dar la respuesta adecuada ante estas
situaciones.

Tendremos que emplear tres funciones y varias variables.
Por suerte FreePascal incorpora la unit Windows donde
están declaradas la mayor parte de tipos de datos e
importadas la mayor parte de funciones de la Win32
API.

La función LoadLibrary, que recibe como
parámetro el nombre de la librería en una cadena
terminada en nulo, devolverá cero si esta librería
no existe. En caso contrario devuelve un valor distinto de
cero.

La función GetProcAddress nos devolverá un
puntero nil si la función que importamos no existe. En
caso contrario nos dará un puntero que podremos enlazar en
una variable de tipo función a fin de poderla ejecutar.
Finalmente la función FreeLibrary libera la memoria y
descarga la librería si es necesario.

Vamos a ver un ejemplo con la función Cuadrado de
la primera librería de ejemplo.

program FuncionDLLDinamica;

uses Windows;

const

NOMBREDLL = 'CuadradosYCubos.dll';

NOMBREFUNCION = 'Cuadrado';

type

TCuadrado = function (a : longint) : longint;
*stdcall*;

var

a : integer;

Cuadrado : TCuadrado;

Instancia : HMODULE; // Tipo de la unit
Windows

begin

Write('Introduzca un numero : ');
Readln(a);

// Intentaremos importar la función

Instancia :=
LoadLibrary(PCHar(NOMBREDLL));

if Instancia <> 0 then

begin

// Hay que realizar un typecasting correctamente del
puntero

Cuadrado := TCuadrado(GetProcAddress(Instancia,
NOMBREFUNCION));

if @Cuadrado <> nil then

begin

Writeln('Cuadrado : ', Cuadrado(a));

end

else

begin

Writeln('ERROR – No se ha encontrado la función
en ', NOMBREDLL);

end;

// Liberamos la librería

FreeLibrary(Instancia);

end

else

begin

Writeln('ERROR – La librería ', NOMBREDLL,' no se
ha encontrado');

end;

end.

Puede parecer un código complejo pero no hay nada que no
se haya visto antes. Comentar sólo que hay que hacer un
amoldado del puntero que se obtiene con GetProcAddress antes de
asignarlo correctamente al tipo función. Como nota curiosa
se puede cambiar 'Cuadrado' de la constante NOMBREFUNCION por el
valor 'Cubo' y todo funcionaría igual ya que Cubo y
Quadrado tienen la misma definición en parámetros y
convención de llamada. Sólo que en vez de obtener
el cuadrado obtendríamos el cubo del
número.

Llamada a funciones del API de
Win32

Aunque la mayoría de funciones de la API se
encuentran importadas en la unit Windows de vez en cuando
tendremos que llamar alguna que no esté importada. El
método es idéntico para cualquier DLL y sólo
hay que tener en cuenta de que la convención de llamada
siempre es stdcall.

Programación orientada a
objetos

Programación procedimental vs
programación
orientada a objetos

Hasta ahora hemos visto un tipo de programación
que podríamos llamar procedimental y que consiste en
reducir los problemas a trozos más pequeños,
funciones y procedimientos, y si es necesario agrupar estos
trozos más pequeños con elementos en común
en módulos, units y librerías.

Este modelo de
programación, que parece muy intuitivo y necesario para
programadores que no han visto la programación orientada a
objetos (POO de ahora en adelante) tiene varios
inconvenientes.

Para empezar, se basa en un modelo demasiado distinto a
la forma humana de resolver los problemas. El ser humano percibe
las cosas como elementos que suelen pertenecer a uno o más
conjuntos de
otros elementos y aplica los conocimientos que tiene de estos
conjuntos sobre cada elemento. Así, es evidente de que un
ratón y un elefante son seres vivos y como seres vivos
ambos nacen, crecen, se reproducen y mueren. En otro ejemplo, un
alambre no es un ser vivo pero sabemos que es metálico y
como elemento metálico sabemos que conduce bien la
electricidad y
el calor, etc.
Esta asociación de ideas a conjuntos no es fácil de
implementar en la programación procedimental ya que si
bien un ser vivo es algo más amplio que el concepto de
elefante no es posible implementar (siempre hablando de
implementación de forma eficiente, claro) un sistema que
dado un ser vivo cualquiera pueda simular el nacimiento,
crecimiento, etc. básicamente porque cada ser vivo lo hace
a su manera.

Llegados a aquí, se empieza a entrever más
o menos el concepto de objeto. Es algo que pertenecerá a
un conjunto y que al pertenecer en él poseerá sus
cualidades a la vez que puede tener sus propias cualidades o
adaptar las que ya tiene.

En qué se parecen una moto y un
camión ? Bien, al menos ambos son vehículos y
tienen ruedas. En qué se distinguen ? La moto
sólo tiene dos ruedas y sólo puede llevar un
tripulante (en el peor de los casos) mientras que el
camión tiene cuatro ruedas y además dispone de un
compartimiento para llevar materiales.
Qué pasa si cogemos una moto y le añadimos una
tercera rueda y un pequeño lugar para un segundo
tripulante ? Pues que la moto se ha convertido en un
sidecar. En qué se distinguen la moto y el sidecar ?
Pues básicamente en sólo este añadido. En
qué se parecen ? En todo lo demás. Por tanto
no es exagerado decir que un sidecar hereda las propiedades de
una moto y además añade nuevas cualidades, como el
nuevo compartimiento.

Ya hemos visto la mayor parte de las propiedades de un
objeto de forma bastante intuitiva que definen los objetos y que
veremos más adelanta. Estas tres propiedades tienen nombre
y son : la encapsulación, la herencia y el
polimorfismo.

Encapsulación, herencia y
polimorfismo

Encapsulación

Quizás ahora no queda muy claro qué quiere
decir encapsulación. Básicamente consisten en que
todas las características y cualidades de un objeto
están siempre definidas dentro de un objeto pero nunca
fuera de los objetos. Es el mismo objeto quien se encarga de
definir sus propiedades y en su turno las implementa. Siempre
dentro del contexto de objeto que veremos.

Herencia

Hablamos de herencia cuando un objeto adquiere todas las
características de otro. Esta característica
permite construir jerarquías de objetos donde un segundo
objeto hereda propiedades de otro y un tercero de este segundo de
forma que el tercero también tiene propiedades del
primero.

En el ejemplo anterior una moto es [heredera de] un
vehículo y un sidecar es [heredera de] una moto. Las
propiedades que definen a un vehículo también las
encontraremos en un sidecar. En cambio a la inversa no siempre es
cierto.

Polimorfismo

Esta palabra sólo significa que dado un objeto
antepasado, o ancestro, si una acción
es posible llevarla a cabo en este objeto (que se
encontrará en la parte superior de la jerarquía de
objetos) también se podrá llevar a cabo con sus
objetos hijos pues la heredan. Pero cada objeto
jerárquicamente inferior puede (que no tiene por
qué) implementar esta acción a su
manera.

Volviendo al ejemplo de los seres vivos : todos se
reproducen. Algunos de forma asexual, dividiéndose o por
gemación. De otros de forma sexual, con fecundación interna o externa,
etc.

Resumiendo

Ahora que hemos visto más o menos cuales son las
propiedades de los objetos podemos llegar a la conclusión
que una de las ventajas directas de la POO es la
reutilización del código y su reusabilidad es
mayor.

Supongamos que tenemos un objeto que, por el motivo que
sea, está anticuado o le falta algún tipo de
opción. Entonces podemos hacer uno nuevo que herede de
éste y que modifique lo que sea necesario, dejando intacto
lo que no se tenga que retocar. El esfuerzo que habremos tenido
que hacer es mucho menor al que hubiéramos tenido que
hacer para reescribir todo el código.

Clases y objetos

De las distintas formas de aproximación a la POO
que los lenguajes de
programación han ido implementado a lo largo del
tiempo, en Pascal encontraremos dos formas parecidas pero
distintas en concepto : los objetos y las clases.

En este manual
emplearemos las clases sin ver los objetos pues es una sintaxis y
concepción mucho más parecida a la forma de
entender la POO de C++ que se basa en clases.

Un objeto es la unión de un conjunto de métodos,
funciones y procedimientos, y campos, las variables, que son
capaces de heredar y extender los métodos de forma
polimórfica. Para emplear un objeto necesitamos una
variable de este objeto. Sólo si tiene métodos
virtuales, que ya veremos que son, es necesario inicializar y
destruir el objeto.

Las clases son como objetos que, a diferencia, no se
pueden emplear directamente en una variable si no que la variable
tiene como finalidad recoger una instancia, una copia usable para
entendernos, de esta clase. Esta
copia la devuelve el constructor y recibe el nombre de objeto ya
que es el elemento real mientras que la clase es un elemento
formal del lenguaje. Posteriormente habrá que liberar el
objeto. Es necesario siempre obtener una instancia de la clase u
objeto, en caso contrario no es posible trabajar con él.
De ahora en adelante cuando hablemos de objetos estaremos
hablando de instancias de clases y no del otro modelo de
aproximación a la POO.

Las primeras clases

Antes de poder trabajar con la POO mediante clases
tenemos que avisar al compilador de que emplearemos clases. Para
hacerlo hay que emplear la directiva de compilador {$MODE
OBJFPC}^5 <#sdfootnote5sym>.

Implementaremos una clase muy simple que permita
realizar operaciones
simples a partir de dos variables de la clase. La clase se
llamara TOperaciones. Las clases hay que definirlas como si
fueran tipos de datos ya que, habitualmente, con lo que se
trabaja es una instancia de una clase, un objeto, no con la clase
en sí.

Para declarar una clase hay que emplear la palabra
reservada class y una estructura algo parecida a un record. Hay
que especificar también de quien es heredera esta clase.
En Pascal todas las clases tienen que heredar de alguna otra y la
clase superior a todas se llama TObject. Esta es la
declaración.

{$MODE OBJFPC}

type

TOperaciones = class ( TObject )

Dentro de esta declaración primero declararemos
los campos, las variables de la clase. En nuestro caso
declararemos los operandos de las operaciones binarias
(operaciones de dos operandos) y les daremos los nombres a y b.
La declaración se hace de la misma forma que cuando
declaramos variables normalmente. Las declararemos de tipo
entero. También declararemos una variable, Resultado, en
la cual guardaremos el valor de las operaciones.

Ahora hay que declarar algún método
(procedimientos y funciones de la clase). En nuestro ejemplo
implementaremos la operación suma en un procedimiento
llamado Suma que sumará a y b y almacenará el
resultado en la variable Resultado. La interfaz de la clase
quedará así.

type

TOperaciones = class ( TObject )

a, b, Resultado : integer;

procedure Suma;

end;

Obsérvese que Suma no necesita parámetros
ya que trabajará con a y b. Una vez tenemos la interfaz
habrá que implementar los métodos. Para hacerlo lo
haremos como normalmente pero cuidando de que el nombre del
método vaya precedido del nombre de la clase. La
implementación de Suma sería la
siguiente :

procedure TOperaciones.Suma;

begin

Resultado := a + b;

end;

Como se ve, podemos trabajar con las variables de la
clase dentro de la misma clase sin necesidad de redeclararlas.
Esto es posible pues la clases tienen una referencia interna que
recibe el nombre de Self. Self se refiere a la misma clase por lo
que la siguiente definición es equivalente a la
anterior :

procedure TOperaciones.Suma;

begin

Self.Resultado := Self.a + Self.b;

end;

El identificador Self lo emplearemos cuando haya alguna
ambigüedad respecto a las variables o métodos que
estamos empleando. En general no suele ser necesario.

Para poder emplear una clase es necesario que la
instanciemos en una variable del tipo de la clase. Para hacerlo
necesitamos emplear el constructor de la clase, por defecto
Create. Finalmente cuando ya no la necesitemos más hay que
liberarla con el método Free. Estos métodos no los
implementamos, de momento, pues ya vienen definidos en TObject.
El código del bloque principal para emplear la clase es el
siguiente :

var

Operaciones : TOperaciones;

begin

Operaciones := TOperaciones.Create; // Instancia de
la clase

Operaciones.a := 5;

Operaciones.b := 12;

Operaciones.Suma;

Writeln(Operaciones.Resultado);

Operaciones.Free; // Liberamos la instancia

end.

Por defecto el constructor Create no toma
parámetros. Su resultado es una instancia de una clase que
almacenamos en Operaciones, como si se tratara de una especie de
puntero. Podemos acceder a los campos y métodos de la
clase como si de un record se tratara. No hay que olvidar de
liberar la instancia pues dejaríamos memoria reservada sin
liberar lo cual podría afectar al rendimiento posterior
del sistema. Téngase en cuenta que una vez liberada la
instancia ya no es posible acceder a sus campos ni a sus
métodos. Hacerlo resultaría en un error de
ejecución de protección general de
memoria.

Hacer clases más
robustas

La clase que hemos diseñado anteriormente tiene
unos cuantos problemas. Para empezar, el programador puede
acceder a la variable Resultado y modificarla a su gusto. Esto
podría falsear el resultado de la operación. Por
otro lado, en una hipotética operación
División el programador podría caer en la
tentación de poner un divisor con valor cero lo que
provocaría un error de ejecución al realizarse la
operación.

Hay varias formas de implementar clases robustas que
sean capaces de reaccionar delante de los errores del programador
y del usuario. Las clases tienen varios mecanismos para proteger
al programador de sus propios errores : los ámbitos
de clase y las propiedades.

ámbitos de la clase o grados de
visibilidad

Las clases definen varios grados de visibilidad. Estos
grados de visibilidad regulan en cierta forma el acceso a los
elementos del objeto, campos y métodos, desde otros
ámbitos del código. Los grados de visibilidad
fundamentales son private y public.

Los métodos y campos con visibilidad private
sólo son accesibles dentro de la misma clase, o sea, en
los métodos que esta clase implementa. Los elementos
public son accesibles desde cualquier lugar desde el cual se
tenga acceso a la instancia de la clase.

Después hay un grado intermedio que recibe el
nombre de protected. A diferencia de private, protected, permite
el acceso a los miembros de la clase pero sólo en las
clases que hereden de la clase, aspecto que en el grado private
no es posible. Como veremos más adelante, las clases
inferiores pueden modificar los grados de visibilidad de
elementos a los cuales tienen acceso en herencia, protected y
public.

En nuestra clase protegeremos la variable Resultado y la
estableceremos private. La resta de clase será public. Los
atributos se fijan directamente en la declaración de la
clase :

type

TOperaciones = class ( TObject )

public

a, b : integer; // Esto es publico

procedure Suma; // Esto también

private

Resultado : integer; // Esto en cambio es
privado

end;

Ahora, tenemos un inconveniente, no es posible acceder a
la variable Resultado fuera de la clase. Una solución
puede pasar por definir una función que devuelva el valor
de Resultado. Una solución más elegante es emplear
propiedades.

Propiedades de una clase

Las propiedades son una aproximación a la POO muy
eficaz que fueron introducidas en Delphi. Son unos miembros
especiales de las clases que tienen un comportamiento
parecido a un campo, o sea una variable, pero además es
posible establecer la posibilidad de lectura y/o
escritura. Lo
más interesante de las propiedades es la posibilidad de
asociar un método con las operaciones de lectura y
escritura con lo cual podemos escribir clases eficientes y
robustas.

Las propiedades se definen de la forma
siguiente :

property NombrePropiedad : tipoDato read
Variable/Metodo write Variable/Metodo;

Téngase en cuenta de que las propiedades
sólo se pueden declarar dentro de una clase. Si omitimos
la parte write tendremos una propiedad de
sólo lectura mientras que si omitís la parte de
read tendremos una propiedad de sólo escritura. Este caso
es técnicamente posible pero es poco útil, en
general.

Para el ejemplo anterior renombraremos la variable
Resultado a FResultado con lo cual tendremos que modificar el
método Suma donde habrá que cambiar Resultado por
FResultado.

type

TOperaciones = class ( TObject )

private

FResultado : integer;

public

a, b : integer;

procedure Suma;

end;

procedure TOperaciones.Suma;

begin

FResultado := a + b;

end;
Aunque el orden es indiferente, es habitual poner los atributos
de visibilidad en el orden : private, protected y public.
Esto es así porque en miembros públicos se emplean
propiedades privadas pero nunca al revés. Declaramos la
propiedad pública Resultado de sólo lectura sobre
la variable FResultado.

property Resultado : Integer read
FResultado;

Cuando leamos el valor de Resultado lo que estamos
leyendo es el valor FResultado. Si intentamos modificar su valor
el compilador nos advertirá de que no es posible pues no
hemos especificado esta posibilidad. La definición de la
clase quedaría tal como sigue :

type

TOperaciones = class ( TObject )

private

FResultado : integer;

public

a, b : integer;

property Resultado : integer read
FResultado;

procedure Suma;

end;

Tal como hemos visto antes en la definición de la
declaración de propiedades es posible indicar que la lectura y
escritura se realice mediante un método. Si el
método es de lectura emplearemos una función del
tipo de la propiedad. Si el método es de escritura
emplearemos un procedimiento con un único parámetro
del tipo de la propiedad.

El programa siguiente es un ejemplo inútil del
uso de las propiedades :

program EjemploPropiedades;

{$MODE OBJFPC}

type

TEjemplo = class (TObject)

private

function LeerPropiedad : Integer;

procedure EscribirPropiedad(Valor :
Integer);

public

property Propiedad : Integer read LeerPropiedad
write EscribirPropiedad;

end;

function TEjemplo.LeerPropiedad :
Integer;

var

i : integer;

begin

Randomize; // Para que los números sean
aleatorios

i := Random(10);

Writeln('Leyendo la propiedad. Devolviendo un ',
i);

LeerPropiedad := i;

end;

procedure TEjemplo.EscribirPropiedad(Valor :
Integer);

begin

Writeln('Escribiendo la propiedad. Ha asignado el valor
', Valor);

end;

var

Ejemplo : TEjemplo;

begin

Ejemplo := TEjemplo.Create;

Writeln('Devuelto : ', Ejemplo.Propiedad); //
Leemos -> LeerPropiedad

Ejemplo.Propiedad := 15; // Escribimos ->
EscribirPropiedad

Ejemplo.Free;

end.

Una posible salida del programa es la
siguiente :

Leyendo la propiedad. Devolviendo un 6

Devuelto : 6

Escribiendo la propiedad. Ha asignado el valor
15

Obsérvese que al leer la propiedad hemos llamado
al método LeerPropiedad que ha escrito la primera
línea. Una vez se ha devuelto un valor aleatorio Writeln
escribe el valor de la propiedad. Después cuando asignamos
la propiedad se ejecuta el método
EscribirPropiedad.

Hay que observar que las referencias a métodos en
las propiedades se hacen igual que las referencias en variables.
También es posible combinar lectura de métodos con
escritura de variables y viceversa sin ningún tipo de
problema. De hecho la combinación mas usual suele ser la
de una propiedad que lee una variable privada y que activa un
método privado cuando se modifica.

Finalmente sólo hay que comentar que la
declaración de métodos de escritura permite que se
pase el parámetro como un parámetro constante. De
forma que la declaración anterior de EscribirPropiedad
podría ser así :

procedure EscribirPropiedad(const Valor :
Integer);

La posterior implementación habría
necesitado también la palabra reservada const.

La función Randomize inicializa los
números aleatorios y la función Random(n) devuelve
un entero aleatorio entre 0 y n-1.

Propiedades indexadas

Las propiedades indexadas son un tipo de propiedad que
permite acceder y asignar datos a través de un
índice como si de un array se tratara. A diferencia de los
arrays usuales, el índice no tiene porque ser un entero,
también es válido un real, cadenas, caracteres,
etc.

Definir una propiedad indexada no es más
complicado que definir una propiedad normal. Simplemente los
métodos write y read añaden los parámetros
del índice. Veamos el ejemplo siguiente:

type

TUnaClasse = class (TObject)

private

procedure SetIndex(a, b : integer; c :
Extended);

function GetIndex(a, b : integer) :
Extended;

public

property Index[a, b : integer] : Extended read
GetIndex write SetIndex;

end;

En este caso si accedemos a la propiedad Index[n, m]
obtendremos un real de tipo Extended. Igualmente, las
asignaciones a Index[n, m] tienen que ser de tipo Extended.
También podíamos haber declarado un tipo string
como índice de la propiedad.

type

TUnaClasse = class (TObject)

private

procedure SetIndex(a : String; b :
Integer);

function GetIndex(a : String) :
Integer;

public

property Index[a : String] : integer read
SetIndex write SetIndex;

end;

Ahora sería legal la sentencia
siguiente :

UnaClasse.Index['hola'] := 10;

13.5.4.Propiedades por
defecto

Si al final de una propiedad indexada añadimos la
directiva default entonces la propiedad se vuelve por defecto.
Obsérvese la declaración
siguiente :

property Index[a : String] : integer read
SetIndex write SetIndex; default;

Ahora sería legal la asignación
siguiente :

UnaClasse['hola'] := 10;

Como se deduce sólo puede haber una propiedad
indexada por defecto en cada clase y las clases descendientes no
pueden modificarla.

Herencia y
polimorfismo

Ventajas de la herencia en la
POO

Los objetos, como hemos comentado antes, son capaces de
heredar propiedades de otros objetos. Algunos lenguajes como el
C++ permiten que una misma clase herede propiedades de uno o
más objetos. La herencia múltiple no deja de tener
sus problemas, como por ejemplo la duplicidad de identificadores
entre clases. Es por este motivo que muchos lenguajes simplifican
la herencia a una sola clase. O sea, las clases pueden heredar
como máximo de otra clase. Esto provoca que las
jerarquías de objetos sean totalmente jerárquicas
sin estructuras
interrelacionadas entre ellas. La aproximación a la POO
que se hace en FreePascal se basa en la herencia simple por lo
que una clase siempre es heredera de una sola cada vez. Esto no
impide que las clases tengan numerosos descendientes.

La clase TObject

Tal como hemos comentado antes, FreePascal implementa la
herencia simple de forma que en algún momento llegaremos a
la clase más superior. Esta clase que ya hemos visto se
llama TObject. En FreePascal todas las clases descienden directa
o indirectamente (ya sea por transitividad de herencia) de
TObject.

La clase TObject define bastantes métodos pero
los más interesantes son sin duda el constructor Create y
el destructor Destroy. El destructor Destroy no se llama nunca
directamente. En vez de ello se emplea otro método de
TObject que es Free. Free libera la clase incluso si no ha sido
asignada por lo que es algo más seguro que
Destroy.

Diseñar clases
heredadas

Una de las ventajas de diseñar clases heredadas
es la de poder dar soluciones
concretas derivadas de
soluciones más genéricas o comunas. De esta forma
nuestras clases se pueden especializar.

La tercera propiedad de los objetos, el polimorfismo,
permite que bajo el nombre de un mismo método se ejecute
el código adecuado en cada situación. Los
métodos que incorporan esta posibilidad reciben el nombre
de métodos dinámicos, en contraposición a
los estáticos, o más habitualmente métodos
virtuales.

Antes de emplear métodos virtuales habrá
que ver ejemplos sencillos, a la par que poco útiles pero
ilustrativos, del funcionamiento de la herencia.

Herencia de clases

Para poder heredar las características de una
clase, métodos y propiedades, habrá que tener
alguna clase de donde heredar. Con esta finalidad declararemos la
clase TPoligono que se encargará teóricamente de
pintar un polígono. De hecho no pintará nada,
sólo escribirá un mensaje en la pantalla. Se supone
que el programador no empleará nunca esta clase sino que
sus derivados por lo que su único método
PintarPoligono será protected. Hay que pasar un
parámetro n indicando el número de lados del
polígono. La clase es la siguiente :

type

TPoligono = class (TObject)

protected

procedure PintarPoligono(n : integer);

end;

procedure TPoligono.PintarPoligono(n :
integer);

begin

Writeln('Pintar polígono de ', n,'
lados');

end;

Hemos supuesto que el programador no trabajará
nunca con objetos de tipo TPoligono sino que sólo sus
derivados. Por eso definimos dos clases nuevas derivadas de
TPoligono : TTriangulo y TCuadrado.

type

TTriangulo = class (TPoligon)

public

procedure PintarTriangulo;

end;

TCuadrado = class (TPoligon)

public

procedure PintarCuadrado;

end;

procedure TTriangle.PintarTriangulo;

begin

PintarPoligono(3);

end;

procedure TQuadrat.PintarCuadrado;

begin

PintarPoligono(4);

end;

Tal como se ve, los métodos PintarTriangulo y
PintarCuadrado no son nada más que casos más
concretos de PintarPoligono. Supongamos ahora que definimos una
clase llamada TPoligonoLleno que además de pintar el
polígono además lo rellena de color.
Será posible aprovechar el código de PintarPoligono
de TPoligono. Lo que haremos es ampliar el método
PintarPoligono en la clase TPoligonoLleno que derivará de
TPoligono. La declaración e implementación es la
que sigue :

type

TPoligonoLleno = class (TPoligono)

protected

procedure PintarPoligono(n : integer);

end;

procedure TPoligonoLleno.PintarPoligono(n :
integer);

begin

inherited PintarPoligono(n);

Writeln('Llenando el polígono de
color');

end;

Para llamar al método de la clase superior que
hemos redefinido en la clase inferior hay que emplear la palabra
reservada inherited. La sentencia inherited PintarPoligono(n)
llama a TPoligono.PintarPoligono(n). De otra forma se
entendería como una llamada recursiva y no es el
caso.

Finalmente declararemos dos clases nuevas
TTrianguloLleno y TCuadradoLleno que descienda de
TPoligonoLleno.

program EjemploHerencia;

{$MODE OBJFPC}

type

TPoligono = class (TObject)

protected

procedure PintarPoligono(n : integer);

end;

TTriangulo = class (TPoligono)

public

procedure PintarTriangulo;

end;

TCuadrado = class (TPoligono)

public

procedure PintarCuadrado;

end;

TPoligonoLleno = class (TPoligono)

protected

procedure PintarPoligono(n : integer);

end;

TTrianguloLleno = class (TPoligonoLleno)

public

procedure PintarTriangulo;

end;

TCuadradoLleno = class (TPoligonLleno)

public

procedure PintarCuadrado;

end;

// métodos de TPoligono

procedure TPoligono.PintarPoligono(n :
integer);

begin

Writeln('Pintar polígono de ', n,'
lados');

end;

// Métodos de TTriangulo

procedure TTriangulo.PintarTriangulo;

begin

PintarPoligono(3);

end;

// Método de TCuadrado

procedure TCuadrado.PintarCuadrado;

begin

PintarPoligono(4);

end;

// Métodos de TPoligonoLleno

procedure TPoligonoLleno.PintarPoligono(n :
integer);

begin

inherited PintarPoligono(n);

Writeln('Llenando el polígono de
color');

end;

// Métodos de TTrianguloLleno

procedure TTrianguloLleno.PintarTriangulo;

begin

PintarPoligono(3);

end;

// Métodos de TQuadratPle

procedure TCuadradoLleno.PintarCuadrado;

begin

PintarPoligono(4);

end;

var

Triangulo : TTriangulo;

TrianguloLleno : TTrianguloLleno;

Cuadrado : TCuadrado;

CuadradoLleno : TCuadradoLlen;

begin

Triangulo := TTriangulo.Create;

Triangulo.PintarTriangulo;

Triangulo.Free;

Writeln; // Una linea de separación

TrianguloLleno :=
TTrianguloLleno.Create;

TrianguloLleno.PintarTriangulo;

TrianguloLleno.Free;

Writeln;

Cuadrado := TCuadrado.Create;

Cuadrado.PintarCuadrado;

Cuadrado.Free;

Writeln;

CuadradoLleno := TCuadradLleno.Create;

CuadradoLleno.PintarCuadrado;

CuadradoLleno.Free;

end.

Como se ve, la POO exige algo más de
código pero no implica que los archivos compilados
resultantes sean mayores. Simplemente la sintaxis es algo
más compleja y se requieren algo más de
código.

El resultado del programa anterior
sería :

Pintar polígono de 3 lados

Pintar polígono de 3 lados

Llenando el polígono de color

Pintar polígono de 4 lados

Pintar polígono de 4 lados

Llenando el polígono de color

Problemas de los métodos
estáticos

Vamos a ver el problema de los métodos
estáticos en POO. Supongamos que definimos una clase
TVehiculo con un método público Ruedas. Este
método será una función que devolverá
el número de ruedas del vehículo. Para el caso de
TVehiculo al ser genérico devolveremos -1. Declararemos
también dos clases descendientes de TVehiculo llamadas
TMoto y TCoche que también implementan la función
Ruedas. En este caso el resultado será 2 y 4
respectivamente.

type

TVehiculo = class (TObject)

public

function Ruedas : integer;

end;

TMoto = class (TVehiculo)

public

function Ruedas : integer;

end;

TCoche = class (TVehiculo)

public

function Ruedas : integer;

end;

function TVehiculo.Ruedas : integer;

begin

Ruedas := -1;

end;

function TMoto.Ruedas : integer;

begin

Ruedas := 2;

end;

function TCoche.Ruedas : integer;

begin

Ruedas := 4;

end;

Si declaramos una variable del tipo TVehiculo y la
instanciamos con los constructores de TMoto o TCoche entonces
podremos acceder al método Ruedas ya que TVehiculo lo
lleva implementado. Esto es sintácticamente correcto ya
que las clases TMoto y TCoche descienden de TVehiculo y por tanto
todo lo que esté en TVehiculo también está
en TMoto y TCoche. La forma inversa, pero, no es posible. Por
este motivo es legal realizar instancias de este tipo.

Supongamos el programa siguiente :

var

UnVehiculo : TVehiculo;

UnCoche : TCoche;

begin

UnVehiculo := TVehiculo.Create;

Writeln(UnVehiculo.Ruedas);

UnVehiculo.Free;

UnCoche := TCoche.Create;

Writeln(UnCoche.Ruedas);

UnCoche.Free;

UnVehiculo := TCoche.Create;

Writeln(UnVehiculo.Ruedas);

UnVehiculo.Free;

end.

El resultado del programa tendría que ser el
siguiente :

-1

44

Pero al ejecutarlo obtenemos :

-1

4-1

Qué es lo que ha fallado ? Pues ha fallado
el hecho de que el método Ruedas no es virtual, es
estático. Este comportamiento es debido a la forma de
enlazar los métodos. Los métodos estáticos,
o no virtuales, se enlazan en tiempo de compilación. De
esta forma las variables de tipo TVehiculo siempre
ejecutarán TVehiculo.Ruedas aunque las instanciemos con
los constructores de clases descendientes . Esto es así
para evitar que una clase inferior cambiara de visibilidad el
método. A qué método tendría que
llamar el compilador ? Por ejemplo, si TCoche.Ruedas fuera
privado a quien habría tenido que llamar
TVehiculo ?

Para superar este problema los lenguajes orientados a
POO nos aportan un nuevo tipo de métodos llamados
métodos virtuales.

Los métodos virtuales

Qué es lo que queremos resolver con los
métodos virtuales exactamente ? Queremos que dada una
instancia de la clase superior a partir de clases inferiores
podamos ejecutar el método de la clase superior pero con
la implementación de la clase inferior. O sea, dada una
variable del tipo TVehiculo creamos la instancia con TMoto, por
ejemplo, queremos que la llamada a Ruedas llame a TMoto ya que es
la clase con la que la hemos instanciado. Porque es
posible ? Básicamente porque los métodos
virtuales se heredan como los otros métodos pero su enlace
no se resuelve en tiempo de compilación sino que se
resuelve en tiempo de ejecución. El programa no sabe a
qué método hay que llamar, sólo lo sabe
cuando se ejecuta. Esta característica de los objetos y
las clases recibe el nombre de vinculación retardada o
tardía.

Para indicar al compilador de que un método es
virtual lo terminaremos con la palabra reservada virtual.
Sólo hay que hacerlo en la declaración de la clase.
Modificamos TVehiculo para que Ruedas sea virtual.

type

TVehiculo = class (TObject)

public

function Ruedas : integer; virtual;

end;

Si en una clase definimos un método virtual y
queremos que en sus clases descendientes también lo sean
tendremos que emplear la palabra reservada override. Los
métodos override indican al compilador que son
métodos virtuales que heredan de otros métodos
virtual.

TMoto = class (TVehiculo)

public

function Ruedas : integer; override;

end;

TCoche = class (TVehiculo)

public

function Ruedas : integer; override;

end;

Esto indica al compilador que las funciones Ruedas de
TCoche y TVehiculo son virtuales. El resultado del programa ahora
será el que esperábamos :

-1

44

Téngase en cuenta de que si en una clase
descendiente no hace el método override entonces este
método volverá otra vez a ser estático, para
esta clase y sus descendientes. Igualmente, si lo establecemos de
nuevo virtual será virtual para las clases que desciendan.
Por ejemplo, suprimiendo la directiva override de la
declaración de TCoche el compilador nos advertirá
que estamos ocultando una familia de
métodos virtuales. El resultado será como si
hubiéramos empleado métodos estáticos. Igual
pasa si lo declaramos virtual de nuevo. En cambio, la clase
TMoto, que tiene Ruedas virtual, funcionaría
correctamente. El resultado del programa
siguiente :

var

UnVehiculo : TVehiculo;

begin

UnVehiculo := TCoche.Create;

Writeln(UnVehiculo.Ruedas);

UnVehiculo.Free;

UnVehiculo := TMoto.Create;

Writeln(UnVehiculo.Ruedas);

UnVehiculo.Free;

end.

sería :

-1

2

ya que ahora TCoche le hemos suprimido override y no es
nada más que un método estático y el
compilador lo enlaza directamente con TVehiculo. Es posible
dentro de los métodos override llamar al método
superior mediante inherited.

Los métodos
dinámicos

Si en vez de emplear virtual empleamos dynamic entonces
habremos declarado un método dinámico. Los
métodos virtual están optimizados en velocidad
mientras que los dinámicos están optimizados en
tamaño del código (producen un código
compilado menor) según las especificaciones de
Delphi.

En FreePascal no están implementados y emplear
dynamic es como escribir virtual, pues se admite por
compatibilidad con Delphi.

Clases descendientes de clases con
métodos override

El funcionamiento es parecido a las clases que derivan
de métodos virtual. Si no añadimos override a la
declaración el método será estático y
se enlazará con el primer método virtual u override
que encuentre. Supongamos que definimos TCochecito, por ejemplo,
que deriva de TCoche.TCochecito.Ruedas queremos que devuelva
3.

type

TCochecito = class (TCoche)

public

function Ruedas : integer; override;

end;

function TCochecito.Ruedas : integer;

begin

Ruedas := 3;

end;

Podemos trabajar con esta clase de la forma
siguiente :

var

UnVehiculo : TVehiculo;

begin

UnVehiculo := TCochecito.Create;

Writeln(UnVehiculo.Ruedas);

UnVehiculo.Free;

end.

Esto es posible ya que si TCoche desciende de TVehiculo
y TCochecito desciende de TCoche entonces TCochecito
también desciende de TVehiculo. Es la transitividad de la
herencia.

Al ejecutar este código el resultado es 3. Pero
si suprimimos override de TCochecito.Ruedas entonces el resultado
es 4 pues se enlaza estáticamente con el primer
método virtual que encuentra hacia arriba de la
jerarquía de objetos, en este caso de
TCoche.Ruedas.

Las clases abstractas

Una clase abstracta es una clase con uno o más
métodos abstractos. Qué son los métodos
abstractos ? Los métodos abstractos son
métodos que no se implementan en la clase actual, de hecho
no es posible hacerlo en la clase que los define, sino que hay
que implementarlo en clases inferiores. Es importante tener en
cuenta de que las clases que tengan métodos abstractos no
se tienen que instancias. Esto es así para evitar que el
usuario llame a métodos no implementados. Si intentamos
instanciar una clase con métodos abstractos el
código se compilará correctamente pero al
ejecutarlo obtendremos un error de ejecución.

La utilidad de las
clases abstractas se basa en definir métodos que se
implementarán en clases inferiores por fuerza, a
menos que las queramos inutilizar para no poderlas instanciar.
Los métodos abstractos tienen que ser virtuales por
definición.

Volviendo a los ejemplos anteriores podíamos
haber declarado el método Ruedas de TVehiculo como
abstracto y así evitar la implementación
extraña que devolvía -1. Para definir un
método como abstracto hay que añadir la palabra
abstract después de virtual.

type

TVehiculo = class (TObject)

public

function Ruedas : integer; virtual;
abstract;

end;

Ahora hay que suprimir la implementación de
TVehiculo.Ruedas. Ahora ya no es posible hacer instancias de
TVehiculo pero si emplear variables de tipo TVehiculo con
instancias de TMoto y TCoche que tienen el método Ruedas
de tipo override. Hay que ir con cuidado con los métodos
abstractos, si una clase no implementa algún método
abstracto de la clase superior entonces la clase también
será abstracta. Si la implementación en clases
inferiores se hace estáticamente entonces el enlace se
resolverá en tiempo de compilación : se
intentará enlazar con el método abstracto y se
obtendrá un error de ejecución. Nótese que
los métodos descendientes directos de un método
abstracto no pueden llamar al método inherited pues no
está implementado. Sí es posible hacerlo en
métodos que no sean descendientes inmediatos de
métodos abstractos, incluso si en algún momento de
la jerarquía el método ha sido abstracto. O sea,
desde TCochecito podríamos llamar a inherited de Ruedas
pero desde TCoche no.

14.5.Y el polimorfismo?

Ya lo hemos visto ocultado en los ejemplos anteriores.
Cuando llamábamos Ruedas desde las distintas instancias
hemos llamado al código adecuado gracias a los
métodos virtuales.

La gracia es poder ejecutar código más
concreto desde clases superiores. Por ejemplo, supongamos una
clase TMusicPlayer. Esta clase implementa 4 métodos
abstractos que son Play, Stop, Pause y Restart. El método
Play hace sonar la música, el
método Stop para la música, el método Pause
pone el sistema musical en pausa y Restart lo
reinicia.

Entonces podríamos implementar clases
desciendentes de TMusicPlayer como por ejemplo TWavPlayer que
haría sonar un archivo de sonido de tipo
.WAV. O TMidiPlayer que hace sonar un archivo MIDI. TMP3Player
que hace sonar un archivo MP3. Y todo
simplemente conociendo 4 métodos que son Play, Stop, Pause
y Restart. Cada clase los implementará como precise, todo
dependerá de qué constructor haya hecho la
instancia. Si lo hemos instanciado con TMP3Player se
llamarán los métodos de TMP3Player, si lo hacemos
con TMIDIPlayer se llamaran los métodos de TMIDIPlayer,
etc.

Conceptos
avanzados de POO

Métodos de clase

En C++ se les llama métodos static (no tiene nada
que ver con los métodos virtual) y en Pascal se les llama
métodos de clase. Un método de clase es un
método que opera con clases y no con objetos tal como
hemos visto con todos los métodos que hemos visto hasta
ahora.

Hasta ahora, para poder emplear los métodos de un
objeto necesitábamos instanciar primero la clase en una
variable del tipo de la clase. Con los métodos de clase no
es necesario ya que trabajan con la clase en sí. Esto
quiere decir que los podemos llamar directamente sin haber tenido
que instanciar la clase. El único caso que hemos visto
hasta ahora era el método Create que se podía
llamar directamente sin tener que instanciar la clase.

A diferencia de los métodos normales, los
métodos de clase no pueden acceder a los campos de la
clase (pues no tienen definida la variable Self). Tampoco pueden
llamar a métodos que no sean métodos de clase ni
mucho menos acceder a propiedades.

La utilidad de los métodos de clase es bastante
restringida al no poder acceder a otros métodos que no
sean también de clase ni a otras variables del objeto. A
diferencia de otros lenguajes orientados a objetos como C++ o el
Java, Pascal
no incorpora un equivalente de "variables de clase" (o variables
estáticas según la nomenclatura de
C++ y Java) y por tanto la utilidad de los métodos de
clase queda bastante reducida. Como sustituto de estas variables
de clase, que en Pascal no existen, podemos emplear las variables
de la sección implementation de una unit, ya que es
posible declarar una clase en la sección interface de la
unit e implementarla en la sección
implementation.

Para declarar un método de clase
añadiremos la palabra reservada class antes de la
definición del método. Como a ejemplo vamos a ver
un método de clase que nos indique cuantas instancias se
han hecho de esta clase. Para hacerlo tendremos que sobreescribir
el constructor y destructor de la clase . Recordemos que el
constructor por defecto es Create y el destructor por defecto es
Destroy. Es posible añadir más de un constructor o
destructor (este último caso es muy poco habitual). Los
constructores y destructores son métodos pro que se
declaran con la palabra constructor y destructor
respectivamente.

Los constructores suelen llevar parámetros que
determinan algunos aspectos de la funcionalidad de la clase.
También inicializan variables y hacen comprobaciones
iniciales. Los destructores, a la contra, no suelen llevar
parámetros (no tiene mucho sentido) y se encargan de
liberar la memoria reservada en el constructor : punteros,
otras clases, archivos abiertos, etc.

Es importante que el constructor antes de hacer nada,
llame al constructor de la clase superior, que acabará por
llamar al constructor de la clase TObject. Esto es así
para asegurarse que la reserva de memoria es correcta. En cambio,
el constructor tiene que llamar al destructor de la clase
superior una vez ha liberado sus datos, o sea, al final del
método. Téngase en cuenta de que en la
definición de TObject Destroy es virtual, de forma que
tendremos que hacerlo override si queremos que todo funcione
perfectamente.

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

Nota al lector: es posible que esta página no contenga todos los componentes del trabajo original (pies de página, avanzadas formulas matemáticas, esquemas o tablas complejas, etc.). Recuerde que para ver el trabajo en su versión original completa, puede descargarlo desde el menú superior.

Todos los documentos disponibles en este sitio expresan los puntos de vista de sus respectivos autores y no de Monografias.com. El objetivo de Monografias.com es poner el conocimiento a disposición de toda su comunidad. Queda bajo la responsabilidad de cada lector el eventual uso que se le de a esta información. Asimismo, es obligatoria la cita del autor del contenido y de Monografias.com como fuentes de información.

Categorias
Newsletter