terça-feira, 31 de julho de 2012

Variáveis dinâmicas




Comparação entre variáveis estáticas e variáveis dinâmicas

Até o presente momento, lidamos com variáveis que tiveram de ser criadas antes de se executar um programa. São variáveis que existem o tempo todo, ou seja, são variáveis estáticas. Portanto, a alocação de memória para esse tipo de variável é feita antes da execução do programa. A grande desvantagem desse tipo de variável é o fato de que uma vez criada, o espaço de memória que ela ocupa não pode mais ser alterado. As variáveis dinâmicas podem ser criadas e ou destruídas durante a execução de um programa, e esta, é a grande vantagem delas sobre as estáticas. As variáveis dinâmicas podem ser obtidas através de um tipo pré-definido em Pascal, chamado Pointer.
O Pointer ou apontador, como o próprio nome diz, aponta para um local de memória onde está armazenada uma variável.

O tipo Pointer

O procedimento para se declarar uma variável do tipo Pointer é simples, senão vejamos:

        Var
                p : ^Integer;

Após esta declaração, teríamos criado uma variável do tipo Pointer que ocupa 4 bytes (lembre-se que ela aponta um endereço, e como sabemos, no IBM/PC, um endereço é formado pelo Segment e pelo offset, cada um com 2 bytes) e que irá apontar uma variável do tipo Integer. Eu utilizei como exemplo, o tipo Integer, mas poderia ser qualquer outro tipo e até mesmo Records.
Até esse instante, não criamos a tão famosa variável dinâmica, e sim uma variável do tipo Pointer, que irá apontar o endereço de uma variável dinâmica do tipo Integer. Isto parece meio complicado a princípio, mas aos poucos, iremos entender o funcionamento desse novo tipo de variável.
E agora eu pergunto, para onde está apontando a variável recém-criada chamada p ? Simplesmente para nenhum lugar. E isto recebe o nome em Pascal de NIL. Quando escrevemos no meio de um programa a declaração abaixo:

        p := NIL;

Estamos querendo dizer que a variável do tipo Pointer, chamada p, não está apontando  para  nenhuma variável no momento. Sempre que criamos uma variável do tipo Pointer, ela tem o valor inicial NIL.

Criação de variáveis dinâmicas

O próximo passo, é a criação de uma variável dinâmica, para tanto, utilizamos a procedure New. Sua sintaxe é:

        New(p);

Isto faz com que seja alocado um espaço de memória, suficiente para armazenar uma variável do tipo associado a p, no caso integer. Esse espaço de memória fica num local especial chamado HEAP. No caso do IBM/PC, o HEAP é toda a memória não utilizada pelo sistema.
Portanto, a declaração New(p) aloca um espaço de memória no HEAP, suficiente para armazenar uma variável do tipo Integer e retorna o endereço inicial desta região de memória para a variável p. Lembre-se que p é do tipo Pointer.

A grande questão agora é: Como acessamos essa variável dinâmica? Através da seguinte simbologia:

        p^

Está na hora de um exemplo para esclarecer melhor as coisas:

Program Exemplo;
        Uses CRT;
        Type  Ponteiro = ^Integer;
        Var p : Ponteiro;
            i : Integer;
(* p é uma variável do tipo Pointer que aponta para variáveis dinâmicas do tipo integer *)
        Begin
           ClrScr;
           If p = NIL Then Writeln('sim');
(* como p acabou de ser criada, ela näo deve estar apontando para algum endereço, ou seja, seu valor inicial deve ser NIL. Para descobrirmos se isso é verdadeiro, basta compará-la com NIL *)

           New(p);

(* acabamos de criar uma variável dinâmica do tipo Integer, e seu endereço foi colocado no Pointer p *)

           p^:=100;

(* estamos atribuindo o valor 100 à variável dinâmica recém-criada *)

           Writeln(p^);
           i:=200;
           p^:=i;
           Writeln(p^);  (* será escrito 200 *)
        (* A função addr(var) retorna o endereço da variável var *)
           p:=addr(i); (* o pointer contém agora o endereço da variável i *)
           p^:=1000;   (* indiretamente estou atribuindo o valor 1000 à variável i *)
           Writeln(i); (* será escrito 1000 *)
        End.

Estruturas de dados com ponteiros

Suponha que você tenha que fazer um programa que terá que ler uma certa quantidade indeterminada de registros do teclado. Você não sabe se serão 10, 100 ou até 1000 registros. A princípio, você poderia super-dimensionar um array, desde que seu computador tenha memória suficiente, mas mesmo assim, corre-se o risco de, no futuro, termos que redimensionar a matriz. Para um caso como este, podemos utilizar o conceito de  variáveis  dinâmicas. Para tanto, devemos declarar um Pointer para uma variável, cuja estrutura seja constituída de dois campos: um contendo o valor propriamente dito que se quer armazenar e o outro apontando para a próxima variável dinâmica.

        Exemplo:

Program Exemplo;
        Uses CRT;

{Este programa lê registros com a estrutura abaixo, até que se digite 'fim' quando é perguntado o nome da pessoa. Repare que o programa tem a capacidade de ler um número ilimitado de registros sem a preocupação de se definir um array e sua respectiva dimensão.}

         Nome  : String[30];
         Sexo  : Char;
         Idade : Integer;
         Altura: Real;
        Type
             Pessoa   = Record
                              Nome  : String[30];
                              Sexo  : Char;
                              Idade : Integer;
                              Altura: Real;
                        End;
             ponteiro = ^Pessoas;
             Pessoas  = Record
                              Valor : Pessoa;
                              Prox  : Ponteiro;
                        End;
        Var
             p,prim : Ponteiro;
        Procedure Linha;
        Var i:integer;
        Begin
           For i:=1 to 80 do write('-')
        End;
        Begin
           Prim:=nil;
           ClrScr;
           Repeat
              Linha;
              New(p);
              Write('Nome da pessoa -----> ');
              Readln(p^.valor.Nome);
              If (p^.valor.Nome<>'fim')
              Then Begin
                      Write('Sexo ---------------> ');
                      Readln(p^.valor.Sexo);
                      Write('Idade --------------> ');
                      Readln(p^.valor.Idade);
                      Write('Altura -------------> ');
                      Readln(p^.valor.altura);
                      p^.Prox:=Prim;
                      Prim:=p;
                   End;
           Until p^.valor.nome='fim';
           ClrScr;
           Linha;
           p:=prim;
           While p<>nil do
           Begin
              With p^.valor do
                 Writeln(nome:30,sexo:5,idade:5,altura:6:2);
              p:=p^.prox;
           End;
        End.

Procedures para variáveis dinâmicas

Dispose

Esta procedure libera o espaço ocupado pela variável em questão que deve ser do tipo Pointer. Ela não mexe com o resto do HEAP. Sintaxe:

        Dispose(Var);

Podemos dizer que Dispose é contrário a New, pois esta aloca espaço no HEAP para determinado tipo de variável enquanto Dispose libera este espaço.

Mark e Release

Como vimos, as variáveis dinâmicas são armazenadas num local de memória especial chamado de HEAP. Esse trecho de memória funciona como se fosse uma pilha. E para controlar o topo da pilha, o Turbo Pascal mantém um apontador. Nós podemos alterar o valor do apontador do topo do HEAP. Não podemos esquecer que alterando-o valor deste apontador, todas as variáveis dinâmicas que estiverem acima deste endereço serão perdidas. A procedure que nos permite alterar o valor deste apontador é a Release e sua sintaxe é:

        Release(Var);

Onde Var deve ser uma variável do tipo Pointer e que deve conter o endereço desejado, para se atribuir ao apontador do topo do HEAP.
Já a procedure Mark nos permitem atribuir, a uma variável do tipo Pointer, o valor atual do apontador do topo do HEAP. Sintaxe:

        Mark(Var);

Estas duas procedures em conjunto nos permite controlar e liberar, quando desejarmos, um trecho de memória do HEAP.

GetMem e FreeMem

Com a procedure New,  podemos alocar espaço necessário no HEAP somente para uma variável de determinado tipo. Com o par Mark e Release ou Dispose, podemos liberar tal espaço no HEAP. Já, as procedures GetMem e FreeMem, podemos alocar o número de bytes que desejarmos, sem estarmos presos a um determinado tipo de variável.

        Sintaxes:

        GetMem(Var,i);

Onde Var é do tipo Pointer e i Integer.

Após esta declaração, teríamos alocado no HEAP, um espaço de memória no HEAP no tamanho de i bytes. O endereço inicial desse trecho de memória é retornado em Var.

        FreeMem(Var,i);

Esta procedure faz exatamente o contrário da GetMem, ou seja, libera i bytes no HEAP a partir do endereço armazenado em Var.

Functions para variáveis dinâmicas

MaxAvail

Retorna um número inteiro, que corresponde ao número de parágrafos (conjunto de 16 bytes) livres disponíveis no maior bloco de espaço contíguo no HEAP.

MemAvail

        Retorna o número de parágrafos disponíveis no HEAP.

Functions




Definição

As funções são muito parecidas com as procedures. A principal diferença é que o identificador de uma função assume o valor de retorno da função. Uma função deve sempre  retornar um valor e em Turbo Pascal, este valor é retornado no nome da função.

Declaração de funções

A declaração de uma função é muito parecida com de uma procedure que por sua vez é parecida com a de um programa, senão vejamos:

Function Nome_da_função(parâmetros) : Tipo_da_função;
        < área de declarações >
        Begin
                corpo da função
        End;
A formação do nome da função deve seguir as mesmas regras para formação de identificadores em Turbo Pascal. Dentro dos parênteses devemos declarar os parâmetros e seus respectivos tipos dos quais a função depende. O tipo de valor retornado pela função também deve ser declarado.
Na  área de declarações, podemos declarar labels, constantes, variáveis e até mesmo Procedures e Functions. Devemos lembrar que tais elementos só poderão ser utilizados dentro do corpo da função, pois são locais a ela. Abaixo, temos o exemplo de uma função.

        Program Exemplo;
        Uses CRT;
        Var x,y : Real;                   (* variáveis globais *)
        Function Soma(a,b:real):real;     (* Soma é uma função que depende de dois parâmetros reais e devolve um valor real *)
        Begin
           Soma:=a+b;                     (* reparem que o valor da funçåo é retornado p. seu nome *)
        End;
        Begin
           ClrScr;
           x:=Soma(4,5);
           y:=Soma(3,6)-Soma(45.5,5.6);
           Writeln(x:10:2,y:10:2);
           Writeln;
           Write('Valor de x --> ');
           Readln(x);
           Write('Valor de y --> ');
           Readln(y);
           Writeln;
           Writeln(Soma(x,y):10:2);
        End.

Devemos  lembrar que o Turbo Pascal possui inúmeras funções de procedures pré-definidas, que iremos ver no decorrer do curso.

        Exemplos:

        Program Fat;
        Uses CRT;
{Programa para calcular o fatorial de um número lido do teclado, usando o conceito de Function}
        Label inicio,fim;
        Var n : Integer;
            tecla : char;
        Function Fatorial(numero:integer) : Real;
        Var i   : Integer;
            Fat : Real;
        Begin (* da função Fatorial *)
           Fat:=1;
           If numero>1
              Then Begin
                      i:=1;
                      Repeat
                         i:=i+1;
                         Fat:=Fat*i;
                      Until i=numero;
                   End;
           Fatorial:=Fat;
        End; (* da função fatorial *)
        Begin  (* do programa *)
           ClrScr;
        inicio:
           Write('Valor de n (menor que 0 = fim) --> ');
           Readln(n);
           Writeln;
           If n<0
              Then Begin
                      Write('Não existe fatorial de numeros negativos');
                      Goto fim;
                   End
              Else Writeln('Fatorial de n = ',fatorial(n):10:0);
           Writeln;
           Goto inicio;
        Fim:
        End. (* do programa *)


        Program Fibonacci;
        Uses CRT;

{Programa para determinar um determinado elemento da seqüência de Fibonacci. A seqüência de Fibonacci é definida como

                        Fib(0)  =  0
                        Fib(1)  =  1
                        Fib(n)  =  Fib(n-1) + Fib(n-2)}

{Como podemos ver, o elemento atual é determinado pela soma dos dois elementos anteriores}

        Label inicio;
        Var numero:integer;
            tecla : char;
        Function Fib(n:integer):integer;
        Var  a1,a2,i,pe : Integer;
        Begin
           if n=0
              Then Fib:=0
              Else If n=1
                      Then Fib:=1
                      Else Begin
                              a1:=0;
                              a2:=1;
                              i:=1;
                              Repeat
                                 pe:=a1+a2;
                                 i:=i+1;
                                 a1:=a2;
                                 a2:=pe;
                              Until i=n;
                              Fib:=a2;
                           End;
        End;
        Begin
           ClrScr;
        inicio:
           Write('Fib(');
           Read(numero);
           Writeln(') = ',fib(numero));
           Writeln;
           Write('Deseja continuar ? --> ');
           Readln(tecla);
           writeln;
           writeln;
           If tecla='s' Then goto inicio;
        End.

Recursividade

A linguagem Pascal e o Turbo Pascal permitem a utilização de funções recursivas. Uma função é dita recursiva quando ela chama a si mesma. Devemos tomar cuidado ao lidar com esse tipo de função, pois podemos criar loops infinitos. Existem pessoas que têm facilidade para  pensar  recursivamente e outras não. A recursividade permite criar funções elegantes e torna os programas mais fáceis de serem entendidos. Abaixo, temos os mesmos programas anteriores, só que utilizando o conceito de recursividade.

        Program Fatorial;
        Uses CRT;
        Label inicio,fim;
        Var n : Integer;
            tecla : char;
        Function Fat(n:integer):real;
        Begin
           if n=0
              Then Fat:=1
              Else Fat:=n*Fat(n-1);   (* repare que estamos chamando novamente a funçåo Fat *)
        End;
        Begin
           ClrScr;
        inicio:
           Write('Valor de n (menor que 0 = fim) --> ');
           Readln(n);
           Writeln;
           If n<0
              Then Begin
                      Write('Não existe fatorial de números negativos');
                      Goto fim;
                   End
              Else Writeln('Fatorial de n = ',fat(n):10:0);
           Writeln;
           Goto inicio;
        Fim:
        End.


        Program Fibonacci;
        Uses CRT;
        Label inicio;
        Var numero:integer;
            tecla : char;
        Function Fib(n:integer):integer;
        Begin
           If n=0
           Then Fib:=0
           Else If n=1
                   Then Fib:=1
                   Else Fib:=Fib(n-1)+fib(n-2);
        End;
        Begin
           ClrScr;
        inicio:
           Write('Fib(');
           Read(numero);
           Writeln(') = ',fib(numero));
           Writeln;
           Write('Deseja continuar ? --> ');
           Readln(tecla);
           writeln;
           writeln;
           If tecla='s' Then goto inicio;
        End.

Procedures


Definição

Uma das técnicas mais utilizadas e tida como vantajosa na confecção de programas grandes é a modularização. Consiste em dividir o programa em diversos módulos ou subprogramas, de certa forma dependentes uns dos outros. Existe um módulo que é o principal, a partir do qual são chamados os outros módulos, esse módulo recebe o nome de programa principal, enquanto que os outros são chamados de subprogramas. No  sistema Turbo Pascal, existem dois tipos de subprogramas, a saber:

                - Procedures (procedimentos)
                - Functions (funções)

A procedure é como se fosse um programa. Ela tem a estrutura praticamente igual a de um programa, como veremos mais adiante. A procedure deve ser ativada (chamada) pelo programa principal ou por uma outra procedure, ou até por ela mesma.

Declaração de procedures

Uma procedure tem praticamente a mesma estrutura de um programa, ou seja, ela contém um cabeçalho, área de declarações e o corpo da procedure. Na área de declarações, podemos ter as seguintes sub-áreas:
           Label - Const - Type - Var - Procedures - Functions.
Devemos salientar que tudo que for declarado dentro das sub-áreas só será reconhecido dentro da procedure. Mais para frente, voltaremos a falar sobre isso.

        Exemplo:

Program Exemplo_1;  (* cabeçalho do programa *)
        Uses CRT;
        Procedure linha;      (* cabeçalho da procedure linha *)
        Var i : integer;      (* subárea Var da procedure linha *)
        Begin                 (* corpo da procedure linha *)
           for i:=1 to 80 do write('-');
        End;
        Begin                 (* corpo do programa principal *)
           ClrScr;
           linha;             (* ativação da procedure linha *)
           writeln('teste');
           linha;             (* ativação da procedure linha, novamente *)
        End.

O programa acima, pura e simplesmente faz o seguinte:

1-) Apaga a tela e coloca o cursor em 1,1
2-) Ativa a procedure linha
3-) Escreve a palavra teste
4-) Ativa novamente a procedure linha.

Por sua vez, a procedure linha traça uma linha a partir da posição atual do cursor. Uma observação importantíssima a ser feita neste instante, é que a variável inteira i, definida dentro da procedure linha só existe dentro da procedure, isto significa que toda vez que ativamos a procedure linha, a variável  'i'  é criada e toda vez que saímos da procedure linha, ela é destruída.

Passagem de parâmetros

No exemplo acima, ao ativarmos a procedure linha, não houve passagem de parâmetros, mas poderia haver, repare no exemplo abaixo:

        Exemplo:

Program Exemplo;
        Uses CRT;
        Var i,j:integer;
        Procedure soma(x,y:integer);
        Begin
           writeln(x+y);
        end;
        Begin
           ClrScr;
           soma(3,4);
           i:=45;
           j:=34;
           soma(i,j);
        end.

Como podemos reparar, a procedure soma depende de dois parâmetros inteiros, e ao ativarmos esta procedure, devemos fornecer os dois parâmetros. Esses parâmetros podem ser dois números inteiros ou duas variáveis inteiras, obviamente deve haver compatibilidade entre os parâmetros passados. Podemos também passar parâmetros de tipos  diferentes, senão vejamos:

Program Exemplo_1;
        Uses CRT;
        Var i,j:integer;
        Procedure soma(x,y:integer;h,g:real);
        Begin
           writeln(x+y);
           writeln(h/g:10:2);
        end;
        Begin
           ClrScr;
           i:=34;
           j:=35;
           soma(i,j,3.4,4.5);
        End.

{Nos exemplos acima, houve passagem de parâmetros para as procedures, mas elas também podem passar dados de volta para o programa chamador, exemplo:}

        Program exemplo;
        Uses CRT;
        Var i : Integer;
        Procedure Soma(x,y:Integer;Var z:Integer);
        Begin
           z:=x+y;
        End;
        Begin
           ClrScr;
           Soma(3,4,i);
           Writeln(i);
        End.

Da forma como foi declarada a procedure soma, quando a ativamos com a seqüência Soma(3,4,i), ocorrem as seguintes passagens:

        - O número 3 é passado para x
        - O número 4 é passado para y
        - O parâmetro z é passado para i.

Como podemos ver, houve passagem de dados do programa chamador para a procedure e da procedure para o programa chamador.

A declaração forward

        Suponha o programa abaixo:

        Program exemplo;
        Uses CRT;
        Procedure Soma(x,y:Integer);
        Begin
           linha;
           Writeln(x+y);
        End;
        Procedure Linha;
        Var i:integer;
        Begin
           For i:=1 to 80 do Write('-');
        End;
        Begin
           ClrScr;
           Soma(3,4);
        End.

Repare que a procedure Soma chama uma procedure chamada linha. No entanto, a procedure linha está declarada mais à frente e portanto, ao compilarmos o programa, o compilador irá "reclamar" que não conhece o identificador  Linha  e com justa razão, isto porque a compilação é feita de cima para baixo e da esquerda para a direita. Para tanto, podemos usar a declaração Forward, cuja finalidade é a de indicar ao compilador que determinada procedure está definida mais para frente.

        Exemplo:

Program exemplo;
        Uses CRT;
        Procedure Linha; Forward;
        Procedure Soma(x,y:Integer);
        Begin
           linha;
           Writeln(x+y);
        End;
        Procedure Linha;
        Var i:integer;
        Begin
           For i:=1 to 80 do Write('-');
        End;
        Begin
           ClrScr;
           Soma(3,4);
        End.

        Agora sim, podemos compilar o programa sem erro.

O escopo de objetos num programa

        Reparem o Exemplo abaixo:

Program Exemplo;
        Uses CRT;
        Const a=100;                     (* constante global *)
        Label fim;                       (* Label global *)
        Var i,x,y : Integer;             (* variáveis globais *)
        Procedure Linha;
        Var i : Integer;                 (* i é local à procedure
                                            linha *)
        Begin
           For i:=1 to 80 do Write('-');
        End;
        Procedure Teste;
        Procedure Sub_teste;              (* a procedure
                                           Sub_teste é local
                                           à procedure Teste *)
        Begin
           Write('Estive em sub_teste');
        End;
        Begin
           Sub_teste;
           Writeln;
        End;
        Begin
           ClrScr;
           i:=100;
           Linha;
           x:=20;
           y:=30;
           Teste;
           Linha;
           Writeln('i=',i,' y=',y,' x=',x);
        End.

Todos os elementos (constantes, variáveis, labels etc.) que forem definidos antes de começar o corpo do  programa, são considerados globais e podem ser utilizados por todas as procedures, functions e o próprio programa. O espaço para tais elementos é criado durante a compilação. Já, os elementos declarados dentro de uma procedure, só existem dentro da procedure, exemplo: ao declararmos uma variável dentro de uma procedure, toda vez que ativarmos a procedure, tal variável será criada e ao sairmos da procedure ela será destruída. Portanto, dizemos que esta variável é local à procedure.
No entanto, se repararmos bem no exemplo, veremos que existe uma variável i inteira declarada antes do início do programa, portanto global, e outra dentro da procedure linha, portanto local a esta procedure. Mas não há problema, pois o Turbo Pascal irá considerá-las diferentes. Quando estivermos dentro do programa, teremos acesso à variável global e quando estivermos dentro da procedure, teremos acesso à variável local.