Linguagens de Programação - …. ilustrar o paradigma dos ADT e introduzir as linguagens...

43
Linguagens de Programação Caderno de Apoio à Cadeira 2000/2001 por Nuno Miguel Cavalheiro Marques 14-12-2000

Transcript of Linguagens de Programação - …. ilustrar o paradigma dos ADT e introduzir as linguagens...

Lingu agens de Programação Caderno d e Apoio à Cadeira

2000/2001

por Nuno Miguel Cavalheiro Marques 14-12-2000

Universidade Aberta -ii - 2000-12-14

Universidade Aberta -i ii- 2000-12-14

Índice Geral

Índice Geral _______________________________________________________________________________iii

Índice de Figuras ____________________________________________________________________________v

Introdução_________________________________________________________________________________vii

1.1 Objectivos da Cadeira ________________________________________________________vii

1.2 Sobre a Necessidade deste Caderno _____________________________________________vii

Módulo de Estudo 1 :Diversidade das Linguagens__________________________________________________1

1.1 Objectivos___________________________________________________________________ 1

1.2 Programas e L inguagens_______________________________________________________1 1.2.1 Principais conceitos_______________________________________________________________1 1.2.2 Tipologia das linguagens___________________________________________________________3

1.3 Pr incipais Paradigmas_________________________________________________________4 1.3.1 Programação Imperativa ___________________________________________________________4 1.3.2 Programação Orientada por Objectos _________________________________________________4 1.3.3 Programação Funcional ____________________________________________________________5

1.4 Evolução e Comparação das L inguagens de Programação ___________________________5 1.4.1 FORTRAN______________________________________________________________________5 1.4.2 COBOL ________________________________________________________________________6 1.4.3 Outras linguagens dos anos 60: O exemplo do APL ______________________________________7 1.4.4 Linguagens da Família do PASCAL __________________________________________________7 1.4.5 C _____________________________________________________________________________9 1.4.6 Linguagens Orientadas por Objectos__________________________________________________9

1.5 Exercícios __________________________________________________________________ 10

Módulo de Estudo 2 : Introdução ao C++________________________________________________________11

2.1 Mater ial de Estudo: __________________________________________________________11

2.2 Objectivos: _________________________________________________________________ 11

2.3 Exercícios __________________________________________________________________ 11

Módulo de Estudo 3 : Elementos duma Linguagem de Programação__________________________________ 13

3.1 Objectivos__________________________________________________________________ 13

3.2 Tipos e Var iáveis ____________________________________________________________13 3.2.1 Nomes ________________________________________________________________________13 3.2.2 Declarações e Associações ________________________________________________________13 3.2.3 Ciclo de Vida de uma Variável _____________________________________________________16 3.2.4 Variáveis Locais Estáticas_________________________________________________________17

3.3 O Tipo de Dados Apontador ___________________________________________________17 3.3.1 R-Value e L-Value_______________________________________________________________17 3.3.2 Reserva Dinâmica de Memória_____________________________________________________17 3.3.3 Dangling Pointers (apontadores pendentes)____________________________________________18 3.3.4 Garbage Colection_______________________________________________________________19

3.4 Expressões e Instruções_______________________________________________________19 3.4.1 Tratamento de Excepções _________________________________________________________20 3.4.2 Tratamento das excepções em JAVA ________________________________________________20 3.4.3 A Instrução goto ________________________________________________________________22

3.5 Sub-programas: Passagem de Parâmetros _______________________________________23

Universidade Aberta -iv- 2000-12-14

3.5.1 Passagem por Valor______________________________________________________________23 3.5.2 Passagem por Referência__________________________________________________________24 3.5.3 Passagem por Valor-resultado______________________________________________________25 3.5.4 Passagem por Nome: Expansão de Macros em C _______________________________________25

3.6 Gestão de Memória __________________________________________________________26 3.6.1 Reserva Estática_________________________________________________________________ 26 3.6.2 Reserva Dinâmica de Blocos_______________________________________________________27 3.6.3 Gestão de Memória nas Linguagens da Família do C ____________________________________27 3.6.4 O Stack-frame dum Programa C ____________________________________________________28

3.7 Exercícios __________________________________________________________________ 29

Módulo de Estudo 4 : Declarações, Definições e Classes em C++_____________________________________31

4.1 Objectivos__________________________________________________________________ 31

4.2 Mater ial de Estudo___________________________________________________________31

4.3 Exercícios __________________________________________________________________ 31

Módulo de Estudo 5 : Introdução à Programação em Lógica ________________________________________33

5.1 Objectivos__________________________________________________________________ 33

5.2 Mater ial de Estudo___________________________________________________________33

5.3 Exercícios __________________________________________________________________ 33

Referências________________________________________________________________________________35

Universidade Aberta -v- 2000-12-14

Índice de Figuras Figura 1 A Máquina de Von Neumann ____________________________________________________________2 Figura 2 : Quando a função foo termina, a àrea de memória que lhe estava reservada é libertada e ptr_a fica com um dangling pointer. ____________________________________________19 Figura 3 : Gestão da memória num programa C/C++ . ______________________________________________28 Figura 4 : Gestão do stack-frame numa chamada de procedimento em C/C++ . ___________________________29

Universidade Aberta -vi- 2000-12-14

Universidade Aberta -vii- 2000-12-14

Introdu ção

1.1 Objectivos da Cadeira Mais do que o estudo de uma única linguagem de programação, a cadeira de Linguagens de Programação da Universidade Aberta vai-se centrar sobre o estudo comparado dos diversos paradigmas e características das linguagens de programação. Assim os alunos vão conhecer os diversos conceitos e técnicas de programação associados aos principais tipos de linguagens de programação (nomeadamente linguagens imperativas, linguagens funcionais, linguagens orientadas por objectos e linguagens lógicas). Na exposição da matéria apresentada, assume-se conhecimento de uma linguagem imperativa, nomeadamente o Pascal (estudada na cadeira de Programação da Universidade Aberta). O conhecimento duma linguagem funcional moderna, como por exemplo o Haskell (estudada na cadeira de Fundamentos da Computação na Universidade Aberta) é também fortemente aconselhado. Como neste momento já deve ser claro para um estudante de informática, a componente prática é indispensável no estudo de qualquer linguagem de Programação. Mais do que uma visão superficial e meramente teórica dos vários paradigmas da programação pretende-se uma visão essencialmente prática. Esta visão só é possível se em vez de se estudarem superficialmente as principais linguagens, nos centrarmos num pequeno conjunto de linguagens modernas, que incorporem todos os paradigmas em estudo. Tal será conseguido através da aplicação dos conceitos teóricos a duas linguagens em concreto o C++ e o Prolog. O estudo da linguagem Prolog será apenas utili zado para ilustrar os paradigmas funcional e lógico, deixando-se o estudo detalhado desta linguagem para uma próxima cadeira. Com o C++ (uma linguagem orientada por objectos híbrida com possibili dade de suporte simultâneo de múltiplos paradigmas), será possível il ustrar como e onde devem ser utili zados os restantes paradigmas e características em estudo.

1.2 Sobre a Necessidade deste Caderno O estudo da cadeira deverá basear-se em 6 meios de estudo principais: 1. Utili za-se este caderno para guiar o aluno pelo estudo dos dois li vros adoptados, complementando-os quando

necessário. Este caderno é constituído por diversos módulos de estudo. Em cada módulo de estudo é feita uma pequena introdução aos conteúdos desse módulo, sendo referidos os objectivos didácticos do módulo e os capítulos e páginas dos li vros adoptados que cobrem essa matéria. Quando necessário será introduzida a descrição de tópicos em estudo, não cobertos pelos dois li vros adoptados. Nesses casos este caderno de apoio será a principal fonte de estudo. Finalmente complementa-se cada secção com um pequeno conjunto de exercícios de autoavaliação para o aluno.

2. O li vro: Problemas e Linguagens de Programação por Helder Coelho, LNEC, 1984. Neste li vro é apresentado um estudo comparativo e uma perspectiva histórica sobre as principais linguagens de programação. São abordados os principais conceitos por detrás dos paradigmas imperativo, funcional e lógico.

3. O li vro: Programação em C++ , conceitos básicos e algoritmos por Pimenta Rodrigues, Pedro Pereira e Manuela Sousa, FCA- Editora de Informática. Servirá simultaneamente para introduzir o paradigma das linguagens orientadas por objectos e abordar a principal li nguagem utili zada na cadeira: o C++. Esta linguagem híbrida, servirá os seguintes objectivos:

1. reforçar os conceitos adquiridos na cadeira de programação com as linguagens imperativas, através duma breve introdução às linguagens da família do C;

2. a utili zação de uma linguagem próxima do sistema operativo como o C/C++, permitirá igualmente compreender a forma de implementação dos principais paradigmas das linguagens de programação;

3. ilustrar o paradigma dos ADT e introduzir as linguagens orientadas por objectos através do estudo de uma linguagem real;

4. permitir aos alunos dominarem os principais conceitos por trás duma das mais populares e poderosas linguagens de programação: o C++;

5. o estudo da linguagem PROLOG permitirá compreender as principais características associadas à programação em lógica.

Universidade Aberta -viii - 2000-12-14

4. Um conjunto de exercícios de auto-avaliação, bem como outra informação complementar para a cadeira estarão brevemente disponíveis, em fase experimental, via Internet na página da cadeira. Quando responder aos exercícios na página Internet, receberá, automaticamente a correcção dos seus exercícios bem como uma informação sobre o seu conhecimento actual do módulo em causa.

5. Um trabalho de desenvolvimento, com entrega facultativa. Parte do exame da cadeira incidirá sobre o estudo deste trabalho.

6. O habitual conjunto com 2 testes formativos e respectivos relatórios, a enviar em breve.

Outras referências aconselhadas, são os li vros:

• "Comparative Programming Languages", 2nd Edition. Leslie B. Wilson e Robert G.Clark, Addison-Wesley, 1993. ISBN 0-201-56885-3. Este li vro aborda, de uma forma comparada, os principais conceitos por trás das diversas linguagens de programação.

• "Programming Languages, Concepts and Constructs", Ravi Sethi, 1st Edition, Addisson-Wesley, September 1989. ISBN 0-201-10365-6, que por vezes serviu de guia em algumas das matérias expostas.

Universidade Aberta -1- 2000-12-14

Módulo de Estudo 1 : Diversidade das Lingu agens A utili zação de uma linguagem de programação é por excelência a forma de especificar o funcionamento de um computador. Consoante os problemas a serem tratados, as melhores formas de especificação e tratamento podem ser distintas. Se incluirmos linguagens específicas e em estudo, não surpreende pois que existam, actualmente, mais de um milhar de linguagens de programação e dialectos distintos. Mais ainda, por todo o mundo comissões internacionais e grupos de investigação continuam a criar, desenvolver, melhorar ou substituir linguagens de programação diferentes. É pois normal a um informático ter contacto com um número elevado de linguagens de programação distintas. Para um estudante que após um esforço inicial já domina razoavelmente uma ou duas linguagens de programação, este panorama pode parecer desanimador: dada a constante evolução da informática, como conseguir rapidamente dominar uma nova linguagem de programação? Qual a melhor linguagem a escolher para reali zar uma determinada tarefa? Para quem verificou como linguagens de programação distintas podem ser diferentes (compare-se por exemplo o Pascal ou o Haskell ), pode à partida parecer uma missão impossível tentar aprender a trabalhar com qualquer nova linguagem, que pode ainda nem existir. Este não é no entanto o caso, pois embora algumas linguagens possam parecer à partida muito diferentes, cedo se começam a encontrar as semelhanças entre as diversas linguagens. Esta caracterização será mais fácil quando enquadrada numa perspectiva histórica de evolução das várias linguagens. Só assim se compreendem as motivações, funções e objectivos da criação dos vários paradigmas relativamente às linguagens de programação e dentro destes das especificidades das várias linguagens. Nesta secção vamos tentar comparar as diversas linguagens de programação entre si, apresentando inicialmente uma caracterização em termos de tipo, prestando particular atenção aos vários paradigmas da computação utili zados. Para auxili ar a compreensão da razão de ser de certas características e do porquê de certas semelhanças será ainda necessário efectuar o estudo da evolução das principais linguagens de programação.

1.1 Objectivos Os objectivos deste módulo são:

1. Compreender os principais conceitos por trás de uma linguagem de programação. 2. Desenvolver a capacidade para distinguir as principais formas de classificação de linguagens. 3. Introduzir os principais paradigmas da programação, associado-os às principais linguagens. 4. Saber associar os diversos paradigmas de programação com as linguagens que lhe deram origem. 5. Identificar os principais erros de estruturação e associar a sua solução com o aparecimento de uma

dada linguagem. 6. Dada uma tarefa e um conjunto de linguagens de programação, saber quais as linguagens mais

apropriadas para resolver esse problema.

1.2 Programas e Lingu agens Segue-se um resumo dos temas abordados no capítulo 1 do segundo li vro adoptado (páginas 1 a 24 de [RodriguesPereiraSousa98]) e da secção "Programas e Linguagens" (secção 3) do 1º li vro adoptado (páginas 17 a 26 de [Coelho84]). Com este resumo pretende-se auxili ar o aluno no estudo dos li vros adoptados, realçando os pontos principais e adicionando alguma informação, onde relevante. A leitura deste resumo não dispensa de forma alguma a leitura das referidas secções.

1.2.1 Principais conceitos Os seguintes conceitos são muitas vezes utili zados nos li vros adoptados e a sua compreensão é essencial na compreensão da forma de funcionamento de um computador e na compreensão do que é e de como funciona uma

Universidade Aberta -2- 2000-12-14

li nguagem de programação. Máquina de von Neumann: Abstracção do computador moderno. É utili zada para ilustrar o funcionamento de um computador. É constituída por uma memória capaz de guardar bytes de informação devidamente endereçados; uma unidade de controle que executa instruções de controle do estado da máquina (incluindo qual o endereço da próxima instrução a ser lida da memória); uma unidade aritmética que executa operações sobre os dados em memória e uma unidade de entrada/saída, que interage com o exterior.

Figura 1 A Máquina de Von Neumann

A programação da unidade de controlo da máquina de Von Neumann pode apenas ser efectuada com um conjunto muito limitado e simples de instruções. Uma hipótese de um conjunto de instruções base para a máquina de von Neumann poderia ser:

• Soma e subtracção de um endereço de memória na posição i, com o acumulador: A := A+M[i]; A:=A-M[i];

• Soma e subtracção do valor endereço dado pelo endereço de memória na posição i, com o acumulador:

A := A+ [M[i]] ; A:=A-[M[i]] ; • Multipli cação e divisão do acumulador por 2:

A := A*2; A:= A div 2; • Mover o conteúdo do acumulador de e para memória:

A :=M[i]; M[i] :=A; • Executar as instruções situadas a partir do endereço i de memória:

Goto i (PC := i) • Se o valor do acumulador for maior ou igual a zero, executar as instrução situadas a partir

do endereço de memória i: if( A>=0 ) goto i

Todos os programas executados num computador têm de ser codificados num conjunto de instruções similares a estas. Os programas assim codificados dizem-se em linguagem máquina ou código máquina. Compilador : Um compilador é um programa, capaz de ler um ficheiro (normalmente de texto) contendo um programa escrito numa dada linguagem de programação e gerar um programa em linguagem máquina que seja equivalente ao programa inicial. Para tal, o compilador deve analisar o programa lido quer sintacticamente (i.e. verificar se a sua escrita está de acordo com uma gramática bem definida) quer semanticamente. A secção 2.3 do li vro [RodriguesPereiraSousa98] (páginas 31 a 36) refere um compilador para a linguagem C++. Interpretador : Tal como o compilador, um interpretador analisa sintáctica e semanticamente uma dada linguagem de programação. No entanto, em vez de converter o programa que lhe é dado em linguagem máquina, o interpretador executa o programa que lhe é dado, passo a passo. Assim é o interpretador que controla o comportamento do programa, facilit ando a interacção com o utili zador e a reescrita do programa (visto que o

Universidade Aberta -3- 2000-12-14

programa não tem de ser recompilado sempre que o desejamos executar). Linguagens de programação: Linguagens formais utili zadas na descrição de mecanismos abstractos. Têm como objectivo descrever e comunicar um processo computacional. Podem ser utili zadas para estudar os algoritmos e para os definir de modo não ambíguo.

1.2.2 Tipologia das linguagens Há diferentes formas para representar e relacionar as linguagens de programação entre si.

a) Por níveis: disposição hierárquica, segundo o seu nível e âmbito. O nível denota um índice da facili dade do programador enunciar a solução dos seus problemas. Linguagens de baixo nível: O código máquina e a sua representação em Assembler. Linguagens de alto nível: algorítmicas, imperativas, prescriti vas, procedimentais (apoiadas sobre processos), determinísticas e quantitativas. Papel determinante do cálculo numérico. Exs: FORTRAN, PASCAL e SIMULA. Linguagens de muito alto nível: Idealmente, não algorítmicas1, declarativas, não determinísticas e qualitativas. Vocacionadas para o processamento simbólico (como cálculo formal, manipulação de fórmulas algébricas, processamento de língua natural), conduzindo a programas mais abstractos. O programador descreve o problema em função de relações sobre objectos. Com uma descrição precisa o computador resolverá o problema sem qualquer outra intervenção. Exs: SQL, PROLOG, Haskell . Devido ao facto de ser ainda hoje uma das principais linguagens funcionais alguns autores incluem igualmente o LISP nesta família; isto apesar desta linguagem ser fortemente algorítmica e já ter sido inclusivamente utili zada na construção de um sistema operativo.

b) Por Árvores genealógicas Desenha-se a rede das linguagens de programação destacando as suas ligações implícitas e explícitas. As principais árvores dividem-se pelos paradigmas da programação: imperativo, orientado por objectos, funcional e lógico.

c) Gerações As linguagens agrupam-se de acordo com um processo de mutações e selecções tecnológicas. Este processo é descontinuo. É usual a seguinte classificação em 5 gerações: 1ª Geração: Linguagem binária, linguagens máquina e Assembly. 2ª Geração: Utili zando compiladores (i.e. baseadas em sistemas que lêem o programa numa destas linguagens e o "traduzem", ou compilam, para linguagem binária): COBOL, BASIC, RPG, FORTRAN IV/V, ALGOL 58/60/68/W. 3ª Geração: Linguagens procedimentais: PL/1, PASCAL, FORTRAN 8X, APL, C, SIMULA, BLISS. 4ª Geração: Geradores de programas (i.e. produzem programas noutras linguagens). A linguagem ADA. Linguagens de interrogação (p.ex. para bases de dados):SQL, QBE. 5ª Geração: Linguagens de específicação de problemas: LISP, PROLOG.

d) Outras Formas de Classificação Eis outras formas de classificação referidas no texto, úteis essencialmente para identificar quando e como se deve optar por uma linguagem ou por outra. Classificação por domínios de apli cação:

• científicas: ALGOL, BASIC, FORTRAN • comerciais: COBOL

1 - De facto é muito difícil criar uma linguagem de programação não algorítmica. Mesmo nas linguagens desta família a programação, muitas vezes, acaba por ser efectuada utili zando um estilo algorítmico.

Universidade Aberta -4- 2000-12-14

• processamento de li stas: LISP • gerais (ou fins múltiplos): ALGOL68, PASCAL, SIMULA 67 • Inteligência Artificial: PROLOG • Ciências Sociais: SPSS. • Planeamento de espaços: PROLOG • Programação de Sistemas: C

Classificação por modos de processamento: • interactiva: o programador pode efectuar alterações ao programa enquanto este é executado: o programa

não é compilado mas sim interpretado. O BASIC foi pioneiro neste conceito. Exs: BASIC, APL, LISP, PROLOG.

• Por lotes (ou programas compilados). Exs: FORTRAN, COBOL, ALGOL, PASCAL. Classificação por categorias: Se a linguagem é específica para um dado fim, ou se pelo contrario, pretende ser o mais universal possível. Definem-se duas categorias principais:

1. Fim geral: PL/1, PASCAL, BASIC, FORTRAN, C. 2. Fim especifico: COBOL, LISP, PROLOG.

Classificação por número de utili zadores: Sobre o número de utili zadores que houve/há de cada linguagem num dado momento, ou pela percentagem do código existente numa dada linguagem.

1.3 Principais Paradigmas Paradigmas (estilos de programação) diferentes são adequados para objectivos diferentes.

1.3.1 Programação Imperativa Linguagens que permitem descrever a resolução de um problema através de uma série de tarefas elementares (comandos) que o computador pode compreender e executar. A sequência de comandos define o processo (ou procedimento) a seguir e permite a mudança de estado de variáveis de atribuição. Implementada com base na máquina de von Neumann. As variáveis do programa (e o número da instrução em execução) descrevem o estado da computação a qualquer instante. Pr incipais problemas:

• Não são adequadas à computação em paralelo (pois estão baseadas em execução sequencial). • Baixa produtividade na programação: Grande esforço do programador no controlo do estado do programa

(valores das variáveis). São exemplos clássicos o FORTRAN, COBOL, PASCAL e C. No segundo li vro adoptado ([RodriguesPereiraSousa98]), na página 17, são identificados mais alguns paradigmas, que estão normalmente associados ao paradigma imperativo, nomeadamente o paradigma procedimental (decomposição do problema em procedimentos), o modular e a abstracção de tipos de dados. Na programação modular o programa divide-se em módulos associados aos vários dados que é necessário tratar. Para cada um desses módulos constróem-se procedimentos que acedem aos dados desse módulo, por forma a impossibilit ar o acesso a esses dados pelo resto do programa (i.e. encapsulamento dos dados). A abstracção dos tipos de dados possibilit a um passo para além da simples modularidade: em vez de se terem procedimentos associados aos dados, constróem-se procedimentos associados ao tipo dos dados. Ambos estes paradigmas são fundamentais para a programação orientada por objectos. E serão estudados em profundidade nos próximos módulos.

1.3.2 Programação Orientada por Objectos O programa é organizado em função de objectos, que contêm não só as estruturas de dados (i.e. os registos do PASCAL) mas também as funções que sobre eles agem. A comunicação entre objectos é feita através de mensagens (que activam as funções de um objecto). Mensagens normalizadas permitem que diferentes classes de objectos possam responder aos mesmos tipos de mensagens. Os objectos são normalmente organizados numa rede podendo os objectos mais especiali zados herdar as propriedades (i.e. funções e campos) dos objectos mais genéricos (herança). Este paradigma está particularmente bem adaptado à construção de interfaces gráficas, conforme é ilustrado em

Universidade Aberta -5- 2000-12-14

[RodriguesPereiraSousa98], na página 19,20. São exemplos clássicos de linguagens com este paradigma o SMALLTALK e o SIMULA. O C++ e o JAVA são linguagens que utili zam o paradigma da programação orientada por objectos, embora também deixem ao programador a possibili dade de ignorar o paradigma da programação orientada por objectos e utili zar apenas o paradigma da programação imperativa. Por esta razão, estas linguagens são por vezes chamadas de linguagens híbridas entre linguagens imperativas e linguagens orientadas por objectos.

1.3.3 Programação Funcional Descrevem o conhecimento de um problema através da declaração de funções (instruções funcionais de alto nível). As funções são aplicadas recursivamente ou por composição e têm como resultado valores. A parte algorítmica e procedimental é (idealmente) suprimida: modela-se apenas as formulações matemáticas da computabili dade. São exemplos clássicos o LISP ou o APL. A linguagem Haskell é também uma linguagem funcional.

a) Programação lógica

O problema é descrito em função de afirmações (asserções ou factos) e de regras sobre os objectos. Procurou-se possibilit ar a total supressão da parte algorítmica e procedimental: cabe ao interpretador (ou compilador) encontrar os caminhos ou processos de resolução do problema. A programação em lógica estende o paradigma funcional. Programas e dados aparecem em conjunto. A programação lógica está normalmente associada à linguagem PROLOG.

1.4 Evolução e Comparação das Lingu agens de Programação Para além de dar ao aluno uma perspectiva histórica da evolução das linguagens de programação, há mais três razões principais para efectuar este estudo: as linguagens actualmente existentes são mais facilmente explicáveis se analisarmos a sua evolução. De igual forma, a análise dos erros feitos no passado permitir-nos-á compreender melhor porque devemos evitar comete-los de novo quando construímos o nosso próprio código. Por último, ao apresentar a forma como estes são implementados em diversas linguagens e as vantagens que a sua inclusão apresenta na escrita do código, este estudo permite exercitar a comparação de várias linguagens e motivar o estudo dos vários paradigmas da programação, O resumo que se segue complementa a leitura das secções "Linguagens de Programação: Evolução e Conceitos" (secção 4 e 6), "Comparação de Algumas Linguagens de Programação" (secção 5) e "Escolha da Linguagem mais Apropriada" do 1º li vro adoptado (páginas 27 a 60 de [Coelho84]), complementada relativamente ao estudo das linguagens C e C++ com a secção "As Linguagens C e C++" (secção 1.5, páginas 21 e 22 do segundo li vro adoptado, [RodriguesPereiraSousa98]).

1.4.1 FORTRAN Em 1954 surge o FORTRAN (iniciais de FORmula TRANslator), criado por John Backus. Foi a primeira linguagem de alto nível. Antes do FORTRAN a programação tinha de ser feita em ASSEMBLY, uma linguagem de programação simbólica, cujo único fim era tornar legível para um humano o código de máquina. Assim enquanto a linguagem ASSEMBLY associava uma instrução simbólica directamente a uma instrução máquina do processador, o FORTRAN compilava (ou traduzia) 1 expressão ou formula de base matemática (representada por 1 instrução FORTRAN) para um conjunto de instruções máquina. Na década de 60 o FORTRAN, entretanto redesenhado, nomeadamente recebendo influencias do ALGOL, torna-se cada vez mais utili zado, chegando a ser considerado como uma normas para as apli cações de cálculo científico. No fim da década de 70, inicio da década de 80, o FORTRAN 77 apresenta-se como uma linguagem de excelência para os cálculos numéricos e científicos. Existem no entanto duas objecções principais aos programas em FORTRAN:

Universidade Aberta -6- 2000-12-14

1. É difícil escrever programas legíveis. É usual a apli cação de alguns truques difíceis de compreender. 2. É difícil estruturar os programas de forma lógica.

Um dos problemas com as versões do FORTRAN anteriores ao FORTRAN 77 era a necessidade de utili zar a instrução GOTO. Esta instrução provoca um salto da linha de execução corrente para outra secção do código. Os problemas surgem porque é muitas vezes necessário associar esta instrução à instrução IF, criando um salto condicional para outra secção do código. Torna-se pois necessário construir cursos de acção demasiado complicados para serem lembrados em detalhe. A instrução GOTO, utili zada desta forma, leva pois a programas ilegíveis. Outro dos problemas com as primeiras versões dos programas em FORTRAN é o facto das variáveis serem seriamente limitadas: todas as variáveis eram visíveis em qualquer parte do programa. Este aspecto exige uma constante atenção do programador aos nomes das variáveis e facilit a a introdução de erros no código difíceis de detectar (devido à utili zação, por erro, de uma dada variável, num dado contexto, levar à alteração do valor dessa variável, noutro contexto um significado totalmente diferente). Finalmente, a ausência de recursividade no FORTRAN77 levantava por vezes sérias dificuldades à estruturação do código e à representação de certos problemas. Devido a ter sido a primeira linguagem de programação de alto nível foi desenvolvido um extenso trabalho e conjunto de bibliotecas para cálculo numérico em FORTRAN. Apesar da linguagem FORTRAN não ter ficado indiferente aos desenvolvimentos das ciências da computação e da maioria das recentes versões do FORTRAN (nomeadamente o FORTRAN 9x) resolverem os problemas aqui referidos, a herança do passado, que torna o FORTRAN apetecível, implica igualmente a ilegibili dade do código: a maioria das rotinas FORTRAN foi desenvolvida em versões antigas do FORTRAN, e logo têm os problemas aqui descritos. Mesmo assim, em certos casos, a solução mais prática continua a passar por se recorrer à utili zação do FORTRAN.

1.4.2 COBOL Encomendada pelo Departamento de Defesa dos EUA em 1959, o COBOL foi considerado como uma norma para o desenvolvimento de apli cações comerciais, sendo uma das linguagens mais utili zadas de sempre. O COBOL é uma linguagem preparada para a manipulação de grandes ficheiros, por programas que permanecem praticamente inalterados. O principal conceito introduzido por esta linguagem foi o da importância da descrição dos dados independentemente do computador utili zado. Era assim fácil gerar os dados num computador e tratá-los noutro. Os programas em COBOL podem pois ser vistos como percursores dos modernos sistemas de base de dados. O COBOL apresenta uma sintaxe muito próxima do inglês, o que facilit a a leitura do programa para alguém que não domine a linguagem. Sendo desenhada com vista ao tratamento de grandes volumes de dados, as possibili dades de cálculo científico desta linguagem são muito básicas. Assim, em aplicações exigindo um grande número de cálculos seria preferível a utili zação de outra linguagem, como por exemplo o FORTRAN. Estas características ficam claras após uma breve análise da linguagem. Relativamente à sua sintaxe um programa COBOL apresenta-se dividido em quatro divisões:

• Identificação do nome do programa fonte (a IDENTIFICATION DIVISION). Contendo o nome do programa, o seu autor e data de criação. Características que facilit am a utili zação do programa num meio empresarial.

• Ambiente (a ENVIRONMENT DIVISION) específica o tipo de computador que compilará e o tipo de computador que executará o programa fonte.

• Uma secção para a descrição dos dados (a DATA DIVISION), que específica o formato dos dados a serem utili zados ou preparados pelo sistema.

• Procedimento do programa (a PROCEDURE DIVISION), que específica qual o tratamento a dar aos dados. O COBOL pode ser considerado como o precursor das modernas linguagens de bases de dados, nas quais o SQL tem vindo a ganhar relevância. De facto, actualmente verifica-se que os primeiros sistemas COBOL tem vindo gradualmente a ser substituídos por sistemas de gestão de bases de dados em SQL. Mas nem todos os problemas são resolvidos utili zando sistemas de bases de dados. Outra das tendências no software desenvolvido comercialmente passa pela utili zação de pacotes proprietários adaptados para as principais tarefas da empresa. Actualmente algumas empresas especiali zam-se no aconselhamento e fornecimento de pacotes de software altamente configuráveis, muitas vezes por recurso a uma linguagem de programação própria.

Universidade Aberta -7- 2000-12-14

No entanto, tal como no caso do FORTRAN, a grande popularidade que esta linguagem teve no passado, nomeadamente na área comercial, reflecte-se em que ainda hoje exista um elevado número de apli cações desenvolvidas em COBOL: muitas vezes pode ser necessário o desenvolvimento e alteração de programas COBOL. Mais ainda, as técnicas e módulos de programação que se apli cam nos pacotes proprietários dos dias de hoje, são facilmente aprendidas utili zando a programação em COBOL. LISP O LISP surge em 1959, por proposta de John McCarthy no MIT. Trata-se de uma linguagem essencialmente virada para problemas de inteligência artificial i .e. tendo como objectivo a manipulação de expressões simbólica complexas. Cria uma perspectiva totalmente nova de ver as linguagens de programação, sendo a primeira linguagem funcional. Assim, veio introduzir uma série de conceitos novos numa linguagem de programação, muitos dos quais só nos anos 70 foram reconhecidos como imprescindíveis:

• Dados e programas são representados de forma uniforme. • Utili za a li sta como estrutura de dados unificadora. • Forma prefixa dos operadores. • Introduz a recursão como estrutura de controle fundamental. • Utili za garbagge collection para gestão da memória, i.e. as estruturas em memória são geridas de

forma automática, sendo as estruturas de dados não necessárias ao programa apenas removidas quando necessário, de forma dinâmica durante a execução do programa.

Os principais elementos desta linguagem são a li sta (constituída por átomos alfanuméricos ou numéricos, ou outras li stas) e a recursividade. O LISP está incluído no grupo das linguagens funcionais. O objectivo destas linguagens é, idealmente, que o utili zador deixe de ser obrigado a descrever com precisão como o resultado deve ser computado, concentrando-se no que deve ser calculado: o resultado. Tanto em LISP como em PROLOG os dados e o próprio programa são representados da mesma forma, sendo fácil ao programa alterar os seus dados, ou mesmo alterar-se a si próprio. Devido a um grande conjunto de funções tanto o LISP como o PROLOG conseguem realizar rapidamente tratamentos bastante complicados. Nesta cadeira iremos dedicar um pouco mais de atenção ao estudo da linguagem PROLOG devido à riqueza das suas técnicas de programação, essenciais para compreender devidamente quer conceitos comuns à programação imperativa como seja o conceito de li sta, de operador e de recursão, quer conceitos introduzidos por estas linguagens, como seja o conceito de gramática formal, análise sintáctica ou de unificação.

1.4.3 Outras linguagens dos anos 60: O exemplo do APL Com a criação da norma BNF, nos anos 60 assistiu-se a uma explosão no número de linguagens de programação. Uma das linguagens de programação mais original deste período foi o APL. A linguagem APL (A Programming Language), foi criada em 1962 por Kenneth Iverson. O objectivo do APL foi tornar a manipulação das matrizes e vectores tão fácil como a de simples números. Esta linguagem veio propor uma nova notação para a computação, incorporando muitos conceitos da matemática, nomeadamente através da utili zação de operadores de muito alto nível. Esta característica veio permitir a escrita mais compacta dos algoritmos, e possibilit ou novas formas de olhar alguns problemas. No entanto, este é também um dos grandes problemas com o APL. Os programas ao serem escritos de forma muito compacta e de forma extremamente críptica (p.ex. tendo os programas de ser lidos da direita para a esquerda), comprometem a sua legibili dade.

1.4.4 Linguagens da Família do PASCAL

a) ALGOL Após o aparecimento do FORTRAN, surge a necessidade de formalizar os conceitos por trás da sintaxe das linguagens de programação de alto nível. Para se poder efectuar esta formalização foi criada a notação BNF (de Backus-Naur Formalism, já estudado na cadeira de Programação da Universidade Aberta, capítulo 2 de [Martins97]). Enquanto notação, o BNF veio permitir a construção de regras gramaticais com uma notação rigorosa e formal. Esta notação foi utili zada na definição da sintaxe para uma nova linguagem: o ALGOL, e veio possibilit ar a reali zação dos trabalhos teóricos que criaram a ciência da computação.

Universidade Aberta -8- 2000-12-14

Surgindo a primeira específicação em 1958, o ALGOL 58 foi a linguagem percursora do PASCAL e do ADA. O ALGOL 58, veio clarificar e adicionar novas características relativamente FORTRAN54. Com o ALGOL58, realçaram-se os seguintes aspectos de uma linguagem de programação:

• Era um linguagem algorítmica i.e. capaz de descrever com facili dade um algoritmo. • Era (tal como o FORTRAN) uma linguagem imperativa. • Criou a noção de bloco (BEGIN ... END, tal como no PASCAL) e procedimento. • Criou o conceito de validação de tipos. • Criou a noção de escopo lexical i .e. um dado nome (p.ex. uma variável ou procedimento, no caso do

PASCAL) só é visível em determinados blocos. • Introduziu as instruções de controlo de ciclos WHILE e DO. • Fez-se acompanhar por um compilador.

Apesar destes aspectos, o ALGOL58, ainda não estava próximo das linguagens mais recentes:

• Não era possível a criação de novos tipos de dados. • Necessitava de um mecanismo abstracto para os dados.

Outro dos principais problemas com a linguagem ALGOL era o esforço de programação que esta linguagem impunha para a entrada e saída de dados não numéricos. Foi no entanto a noção de compilador que veio a permitir a explosão de linguagens que se deu de seguida, durante a década de 60. Em 1966 Niklaus Wirth apresentou o ALGOL-W, que veio de seguida a dar origem ao PASCAL.

b) Pascal Em 1968 Niklaus Wirth, projectou o PASCAL. Esta linguagem pretendeu incorporar os melhores aspectos e facili dades das linguagens ALGOL 60 e ALGOL W. Tendo como objectivo o ensino da programação, oferece novas facili dades para estruturar dados (os tipos de dados definidos pelo utili zador) e a decomposição modular de proporcionou um novo grau de abstracção, os quais foram mais tarde desenvolvidos pelo próprio Wirth na linguagem MODULA-2, que surge na linha do PASCAL. Nas principais características do PASCAL incluem-se:

• A necessidade de declaração de todas as variáveis no inicio do programa/sub-programa. • O suporte de variáveis com qualquer comprimento e tipo, definido na declaração da variável. • A elevada autonomia dos procedimentos, que podem ser escritos e testados independentemente do código

do programa principal. Um dos principais problemas do PASCAL em aplicações de cálculo científico é no entanto o reduzido número de funções matemáticas que suporta, relativamente, por exemplo, ao FORTRAN.

c) ADA

Surge no fim da década de 70, tendo sido escolhida pelo Departamento de Defesa dos EUA para a substituição da infinidade de linguagens que este departamento util izava, nomeadamente do FORTRAN, COBOL e PASCAL. Assim o ADA foi desde cedo virado para problemas de grandes dimensões, complexos e exigindo a manipulação de processos concorrentes. Devido a uma grande exigência a nível do rigor sintáctico dos seus programas, estes apresentam um baixo número de erros e uma elevada fiabili dade. Devido à maior maturidade da Engenharia de Software na altura da sua especificação, o ADA recebeu os principais paradigmas da programação. Assim o ADA é considerado por alguns autores como a primeira linguagem universal, pois incorpora:

• O conceito de dados abstractos. Em ADA, os programas só podem ter acesso a um conjunto seleccionado de informação de um dado módulo.

• A utili zação de estruturas modulares e especificações de interfaces para os grandes programas. • Controle sobre aspectos implementacionais de baixo nível, na máquina objecto. • A manipulação de excepções (surgida na década de 60 na linguagem PL/1, um esforço da IBM para

substituir o FORTRAN, ALGOL e COBOL).

Universidade Aberta -9- 2000-12-14

• O suporte para concorrência e processamento paralelo. Onde são necessários conceitos de como a sincronização de tarefas e de passagem de mensagens entre tarefas em execução simultânea em um ou vários processadores.

• Nas versões mais recentes (Ada95), os conceitos de herança e objecto. Outra das características do ADA é uma elevada fiabili dade do código desenvolvido. Esta característica deriva da elevada rigidez sintáctica e semântica desta linguagem, que permite diminuir o número de erros de funcionamento dos programas. Esta é de resto a razão porque esta é uma boa linguagem para utili zar em sistemas críticos, como no caso de sistemas de controle militar ou aeroespaciais, onde um único erro de programa pode trazer graves consequências. Apesar destas características, o ADA (ainda?) não ganhou grande aceitação. Talvez tal se deva igualmente à rigidez sintáctica que exige na escrita dos seus programas: por vezes, devido a uma especificação sintáctica demasiado exigente, o ADA obriga a soluções menos directas para o desenvolvimento do código. Neste aspecto o ADA distingue-se do C++ que de certa forma se baseia no conceito oposto: o da flexibili zação da linguagem.

1.4.5 C A linguagem C foi construída em 1972 nos Laboratórios Bell por Dennis Ritchie, com o fim de escrever o sistema de operativo UNIX. Trata-se de uma linguagem bastante pequena, apresentando apenas as características indispensáveis. No C todos os aspectos não indispensáveis na linguagem são passados para uma extensa biblioteca de funções, que pode ser expandida pelo programador. Tendo sido a linguagem utili zada para construção dos sistemas UNIX, o C é uma linguagem com extenso acesso ao hardware, com elevada fiabili dade no tratamento das entradas e saídas do sistema. De igual forma, em sistemas UNIX, como exemplo o LINUX, todas as bibliotecas do próprio sistema operativo têm uma interface em linguagem C, podendo pois ser facilmente evocadas de dentro de um programa C. Contrariamente ao PASCAL ou ao ADA, que visam encoraja o desenvolvimento de programas fiáveis através da imposição de uma estrutura, o C é altamente flexível (permitindo quase todas as combinações operando/operador possíveis) e produzindo código extremamente eficiente. A linguagem C é extremamente prática, podendo os programas em C ser quer extremamente compactos (e cripticos), como extremamente estruturados e auto explicativos, sem que com isso se perca em eficiência. Desta forma a decisão de tornar o código claro (quando trabalhando em equipa, ou em projectos largos e desenvolvendo código mais complexo), quer a de tornar o código compacto (quando é necessária eficiência no desenvolvimento ou no código) cabe ao programador. O C é uma linguagem desenvolvida a pensar em programadores responsáveis, sendo dever dum bom programador de C tornar o seu código auto explicativo por forma a respeitar os conceitos da programação estruturada. A linguagem C é uma das linguagens general-purpose mais escolhida pela generalidade dos programadores.

1.4.6 Linguagens Orientadas por Objectos

a) SIMULA 67 O SIMULA67 foi desenvolvido por Dahl e Nygaard, para apli cações no domínio da simulação. Veio introduzir o conceito de classe. Neste conceito um grupo de declarações e procedimentos eram agrupados e tratados como uma única entidade. Durante a execução do programa, podiam ser criados diversos objectos de uma dada classe, cada um trabalhando sobre os seus dados. O conceito de classe ficou associado ao conceito de abstracção de dados.

b) C++ Apesar do grande sucesso da linguagem C entre a comunidade de programadores, e apesar da grande flexibili dade desta linguagem, o suporte dos paradigmas dos tipos de dados abstractos e da programação orientada por objectos é de difícil implementação em C standard. Devido à grande potencialidade destes paradigmas, em especial no que se refere a projectos de trabalho em equipa e à reutili zação de código, em 1988, também nos Laboratórios BELL, Bjarne Stroustrup desenvolveu o C++ como extensão ao ANSI C. Esta linguagem híbrida, nada perde relativamente ao C (pode-se programar em C, utili zando C++), mas estende o C com a possibili dade de utili zar os conceitos da programação orientada por objectos, nomeadamente os conceitos de:

Universidade Aberta -10- 2000-12-14

classe, herança, objectos e de polimorfismo. Sendo uma das linguagens híbridas general-purpose mais completa, ela é a ideal para ilustrar os principais paradigmas da programação. O C++ será a linguagem que iremos estudar em profundidade na cadeira de linguagens de programação. Com exemplos em C++ iremos ilustrar os principais paradigmas e métodos da programação estruturada.

1.5 Exercícios Na página web da cadeira poderá encontrar a li sta de exercícios proposta para este módulo de estudo.

Universidade Aberta -11- 2000-12-14

Módulo de Estudo 2 : Introdu ção ao C++ Inicia-se neste módulo o estudo das linguagens da família do C, em concreto o C++. Com este estudo pretende-se que os alunos tenham o primeiro contacto com uma das linguagens de programação mais poderosas e que está entre as linguagens que geram os programas em código de máquina mais eficientes dos dias de hoje. Mantendo as características de elevada flexibili dade e proximidade ao sistema operativo do C, o C++ estende esta linguagem com um conjunto de paradigmas que em muito facilit am o desenvolvimento de software.

2.1 Material de Estudo : O estudo deste módulo será reali zado utili zando, como material de apoio teórico, os capítulos 2 e 3 do 2º li vro adoptado (páginas 25 a 145 do li vro [RodriguesPereiraSousa98]). Chama-se a atenção para o facto de o CD-ROM que acompanha o li vro conter um conjunto de acetatos que poderá auxili ar o aluno no seu estudo. Como complemento ao estudo teórico, os alunos deverão utili zar um compilador de C++, como seja o Borland C++ ou o Visual C++ para introduzirem, executarem e analisarem (introduzindo se necessário pequenas alterações no código), os exemplos apresentados ao longo do texto. Caso não tenha acesso a nenhum dos compiladores de C++ referidos no li vro (Borland C++ ou o Visual C++), poderá fazer o download (gratuito) de um compilador de C++ via Internet. Caso opte por trabalhar em ambiente Windows, na página Internet da Borland (www.borland.com), tem acesso aos manuais de ajuda e FAQs dos produtos desta companhia e pode após registo no “museu” , fazer download quer da versão comand-line do Borland C++ 5.5, quer do Borland Turbo Debuger 5.5. Com este debuger pode executar os programas compilados em C++, passo a passo, tal como já fez na cadeira de Programação com o Turbo Pascal. Na página web da cadeira poderá obter a ultima informação sobre a forma de instalar e utili zar este software.

2.2 Objectivos: Os objectivos deste módulo são:

• Os objectivos apresentados na página 25 de [RodriguesPereiraSousa98]. • Saber introduzir no computador os programas que escreveu, nomeadamente:

• Compilar o código do seu programa. • Depurar e analisar o código do seu programa por utili zação do Debuger.

• Os objectivos apresentados na página 85 de [RodriguesPereiraSousa98]. • Criar e corrigir sintáctica e semanticamente novos programas, por forma a (com auxili o do

computador), conseguir dos seus programas o comportamento pretendido.

2.3 Exercícios No fim do estudo do capítulo 2, o aluno deve ser capaz de resolver a totalidade dos exercícios propostos nas páginas 79 a 83 de [RodriguesPereiraSousa98]. Em particular o aluno deverá resolver os exercícios 1 a 7, 9, 14, 25 e 26. No fim do estudo do capítulo 3, o aluno deve ser capaz de resolver a totalidade dos exercícios propostos nas páginas 142 e 143 de [RodriguesPereiraSousa98]. Em particular o aluno deverá resolver os exercícios 1 a 8 e 12.

Universidade Aberta -12- 2000-12-14

Universidade Aberta -13- 2000-12-14

Módulo de Estudo 3 : Elementos duma Lingu agem de Programação Neste módulo de estudo tentar-se-á generali zar os conceitos aprendidos no módulo anterior a qualquer linguagem de programação imperativa. De igual forma serão apresentados os principais conceitos associados à gestão de nomes numa linguagem imperativa. Estes conceitos serão essenciais para o estudo da próxima secção. Apesar do 2º li vro adoptado conter uma secção sobre nomes e tipos, esta apresenta-se simultaneamente muito virada para o C/C++, e pouco relacionada com a compreensão do funcionamento interno dum programa de uma dada linguagem de programação. Apenas com o estudo de conceitos mais genéricos será possível compreender como pode um programa escrito numa dada linguagem de programação funcionar num computador segundo o paradigma da máquina de Von Neumann. Revelou-se pois indispensável a introdução desta secção, não só para a correcta compreensão dos conceitos desta cadeira como também como ferramenta indispensável à correcta compreensão do conceito de apontador: um dos principais problemas na aprendizagem da linguagem C e C++. O texto desta secção foi largamente influenciado pelos dois li vros da bibliografia complementar: [WilsonClark93] e [Sethi89].

3.1 Objectivos Com este módulo pretende-se:

• Compreender a importância dos conceitos de bloco de código e procedimento. • Compreender os diversos conceitos associados a um nome em qualquer linguagem de programação:

Valor e tipo, visibili dade e posição. • Compreender o conceito de apontador e a sua relação com a memória interna do computador. • Generali zar o conhecimento das principais estruturas de controlo de um programa escrito numa

linguagem imperativa. • Identificar e saber utili zar as diversas formas de passagem de parâmetros para um procedimento em

qualquer linguagem de programação. • Compreender como um programa em C gere a memória do computador.

3.2 Tipos e Variáveis

3.2.1 Nomes Um programa pode ser visto como um conjunto de operações sobre um conjunto de dados. Sobre os dados desse programa pode ser definido o seu tipo. O tipo dos dados define o conjunto de valores possíveis desses dados e o conjunto de operações que a eles podem ser apli cados. Assim, qualquer conjunto de dados tem um valor e um tipo. Num programa a forma usual de guardar dados é utili zando variáveis. As principais características de uma variável são o seu nome, o seu valor e a referência à área de memória interna do computador (no sentido da maquina de von Neumann atrás referida) que guarda o valor da variável. Assim, para todas as variáveis em todas as linguagens de programação, existe sempre, para qualquer variável:

• nome da variável (também conhecido pelo seu identificador). • a sua área de armazenamento (i.e. o endereço de memória onde o valor dessa variável é guardado). • o valor armazenado.

No entanto, muitas vezes, é usual a referência a "uma variável" denotar qualquer um destes três aspectos, e é por isso necessário saber sempre ao que nos estamos a referir. Devido ao seu poder, a linguagem C (e a sua extensão o C++) recorre extensamente a esta distinção. Sendo uma das principais dificuldades de quem começa a aprender C e C++ efectuar correctamente a distinção entre estes vários conceitos.

3.2.2 Declarações e Associações Outro dos aspectos importantes a ter em conta é o momento da associação do tipo e área de armazenamento a uma

Universidade Aberta -14- 2000-12-14

dada variável. Quando essa associação é efectuada em tempo de compilação ganhamos em eficiência (pois não é necessário executar esta operação durante a execução do programa), por outro lado quando efectuamos essa associação em tempo de execução do programa ganhamos em flexibili dade (pois o programa pode definir de que forma deve essa associação ser efectuada). Declaração de var iáveis: Nas linguagens de programação mais recentes, as variáveis são introduzidas num programa através da sua declaração. É na declaração duma variável que se explicita qual o seu tipo e se indica ao computador a necessidade de reservar um espaço em memória para conter o valor dessa variável. As declarações podem ser efectuadas juntamente com as instruções do programa. Blocos: O PASCAL, é uma linguagem que utili za o conceito de bloco. Em PASCAL um bloco é uma sequência de instruções entre BEGIN e END. Em PASCAL uma dada variável pode ser associada a um bloco (sendo declarada num PROCEDURE ou FUNCTION associado a um conjunto de instruções). Em C, os blocos são declarado pelos caracteres '{ ' (o inicio do bloco) e '} ' (o fim do bloco). O C é mais genérico que o PASCAL, pois qualquer bloco pode conter no inicio a declaração de uma variável. Scope rules (regras de visibili dade de uma variável):

• variáveis locais: Uma variável local é aquela que é declarada dentro dum bloco de código, sendo apenas visível dentro desse bloco.

• variáveis não locais: As variáveis declaradas num bloco que contenha outro bloco dentro de si, são chamadas de não locais quando no bloco mais interno, mas continuam a ser visíveis.

• variáveis globais: Uma variável não local, declarada no bloco mais externo (o programa) é também chamada de variável global.

Exemplo 1: no seguinte programa C:

int a; float b; main() { /* inicio do bloco main */ int c; a=5; for(c=1;c<a; c++) { /* inici o do bloco for */ int j; int a; a=3; } /* fim do bloco for */ } /* fim do bloco main */

a e b são variáveis globais, e logo conhecidas em todo o programa. c é uma variável local ao bloco main e logo não local (mas conhecida) no bloco do ciclo for. Já j é uma variável apenas conhecida no interior do bloco do ciclo for. A variável global a, foi redefinida no interior do bloco do ciclo for, assim dentro do bloco do ciclo for (i.e. entre { e } )é uma nova variável (i.e. é associada a uma nova área de memória). Esta variável local mantém o nome, escondendo por isso a variável global: no interior deste bloco a variável global a não está visível, referindo-se todas as operações sobre a à variável local. O ciclo continua a ser executado 5 vezes, pois embora quando se entra/sai do bloco no interior do ciclo for, a continua a ter o valor de 5. Apenas durante o ciclo a nova variável a passa a ter o valor 3. Uma vez terminado o ciclo, a variável retorna o seu valor original de 5. static scope (i.e. visibili dade estática): Em linguagens como o FORTRAN, C, Pascal, Ada e Modula-2, a visibili dade de um identificador e o endereço de memória ao qual este está associado são determinados em tempo de compilação. Como consequência os nomes são associados aos tipos em tempo de compilação.

Universidade Aberta -15- 2000-12-14

dynamic scope: (visibili dade dinâmica) A associação entre um identificador e a sua declaração (i.e. um local na memória interna do computador), depende da execução do programa. Assim esta atribuição só pode ser efectuada em tempo de execução. Exemplo 2: Considere o seguinte programa numa linguagem hipotética X (idêntica ao C, mas com dynamic scope):

int a=3; float b; int f() { /* inicio da função a */ printf("%d \ n",a); } p5() { /* ini cio do bloco p5 */ int a=5; int c; f(); } /* fim do bloco p5 */ p7() { /* inicio do bloco p7 */ int a=7; int c; f(); } /* fim do bloco p7 */ main() { /* inicio do bloco main */ f(); }

Este programa poderá imprimir no terminal o valor 5, 7 ou 3 consoante a função f seja chamada de dentro da função p5, p7, ou de dentro do bloco main: numa linguagem utili zando visibili dade dinâmica a declaração mais recente (durante a execução do programa) é a utili zada para definir a variável. Assim, se f for chamada de dentro de p5, receberá a variável declarada em p5 (a declaração mais recente de a, pela ordem de execução), se for de p7 receberá a declaração de p7 e, no caso normal para o código apresentado, se for chamada directamente de main, receberá a declaração da variável global. Realce-se, de novo, que isto só será verdade no caso de uma linguagem com visibili dade dinâmica, como é o caso da linguagem X aqui apresentada. Assim, na linguagem C (que inspirou toda a sintaxe da linguagem hipotética, X), por exemplo, este programa imprimiria sempre o valor 3, pois no C as regras são as da visibili dade estática (na função f, a é sempre a variável global). Bastará tentar analisar o código do último exemplo, quer utili zando as regras de visibili dade estática (i.e. considerando o último exemplo como um programa em C) quer utili zando as regras de visibili dade dinâmica (na linguagem hipotética X, do exemplo), para se compreender que as regras de visibili dade estática dão origem a programas muito mais fáceis de compreender e de ler. Se conjugarmos a maior dificuldade de leitura de programas utili zando regras de visibili dade dinâmica com a maior ineficiência do código gerado pelos compiladores com visibili dade dinâmica (pois não é possível efectuar validação de tipos em tempo de compilação, tornando-se pois necessário efectuar essa validação em tempo de execução), compreende-se que, regra geral a visibili dade dinâmica não seja uma das características das linguagens modernas. Há no entanto casos em que a técnica da visibili dade dinâmica apresenta vantagens, trata-se do caso dos tipos dinâmicos. Tipos dinâmicos: uma linguagem com tipos dinâmicos é aquela em que o tipo de uma variável depende do seu valor actual. Os tipos dinâmicos são muito utili zados em linguagens funcionais e linguagens lógicas. É o caso de linguagens como o LISP ou o Prolog. As linguagens APL e SNOBOL4 utili zam igualmente tipos dinâmicos. Como se tratam de linguagens tradicionalmente interpretadas, a sobrecarga de validação de tipos é reduzida, e é largamente recompensada pela flexibili dade ganha. Algumas linguagens funcionais recentes, como o ML ou o Haskell (cadeira de Fundamentos da Computação da

Universidade Aberta -16- 2000-12-14

Universidade Aberta), apresentam um sistema de inferência de tipos. Embora estas linguagens apresentem um sistema de tipos forte, o programador não tem de declarar todos os tipos, pois o sistema pode deduzi-los do contexto em que são utili zados. Mais vulgar é a verificação de tipos que existe nas principais linguagens imperativas: staticall y typed (tipos estáticos): Nas linguagens staticall y typed o compilador pode verificar se as atribuições dos tipos são correctamente efectuadas pelo programador. Assim os programas nestas linguagens tendem a ser: mais seguros (os erros de atribuição de tipos podem ser encontrados em tempo de compilação); mais eficientes (visto que as verificações de correcção dos tipos não necessitam de ser efectuadas durante a execução do programa); e mais fáceis de compreender (visto que a relação entre o tipo de uma variável e o seu identificador podem ser efectuadas por análise do código do programa). Linguagens como o C (através de avisos), Pascal e Modula-2, são staticall y typed. strongly typed: Aquelas linguagens em que o tipo de todos os objectos pode ser predeterminado são chamadas linguagens strongly typed (com identificação de tipos forte). São exemplos de linguagens deste tipo o ADA e o ALGOL68, no entanto a maioria das linguagens não é strongly typed.

3.2.3 Ciclo de Vida de uma Variável Em C, Pascal (mais genericamente, em todas as variantes da linguagem ALGOL), apenas se reserva espaço para uma variável em memória quando a execução do programa entra no bloco em que essa variável é declarada. De igual forma, nestas linguagens, sempre que a execução do programa sai dum bloco, toda a memória reservada para as variáveis declaradas nesse bloco deve ser libertada. Exemplo 3: no seguinte programa C:

int a; /* variavel a global */ float b; int p1() { int a; /* variável a interna a p1*/ ... if(x!=0) p1(); } main() { /* inicio do bloco main */ int c; p1(); } /* fim do bloco main */

enquanto o espaço para as variáveis globais a e b é reservado quando o bloco principal é activado (no inicio do programa), e a e b são pois variáveis globais, conhecidas durante a execução de todo o programa. Já o espaço para a variável a (interna a p1) é reservado sempre que o procedimento p1 entra em execução. Note-se em concreto que se trata de um procedimento recursivo: em cada nova execução do procedimento, é criada uma nova variável a (i.e. é reservado mais um conjunto de endereços na memória interna do computador) e esse espaço é associado à variável criada. De igual forma sempre que termina uma execução do procedimento p1 a variável a associada a essa execução é libertadas. Desta forma sempre que se chama de novo o procedimento p1, podemos garantir uma das características mais importantes na estruturação deste tipo de linguagens: os valores atribuídos às variáveis nessa execução da função não interferem com valores atribuídos em outras execuções dessa função. Este comportamento é indispensável para o suporte da recursividade na linguagem.

Universidade Aberta -17- 2000-12-14

3.2.4 Variáveis Locais Estáticas Pode ser útil preservar os valores de uma variável interna de um procedimento entre diferentes execuções. Em PASCAL, este comportamento só pode ser obtido utili zando variáveis globais. Assim, para aumentar o ciclo de vida de uma variável (por forma a ela ser lembrada entre várias execuções de um mesmo procedimento), somos também forçados a aumentar a sua visibili dade. Apesar da variável ser apenas relevante para um dado procedimento, todo o programa pode alterar o seu valor. O ALGOL 60, o PL/I e o C permitem a declaração das chamadas variáveis locais estáticas. Estas variáveis têm as mesmas regras de visibili dade que as variáveis locais normais, no entanto a reserva do espaço para estas variáveis ocorre, tal como nas variáveis globais, quando o programa é iniciado. Em C uma variável pode ser declarada como variável local estática, precedendo a sua declaração da palavra chave static.

3.3 O Tipo d e Dados Apon tador

3.3.1 R-Value e L-Value Considere-se a seguinte atribuição:

y := y+1 Nesta instrução o valor de y é incrementado de 1. A interpretação de y varia no entanto constante o termo que consideramos. No termo da esquerda, y refere-se ao local onde o valor pode ser guardado (i.e. na variável y). No termo da direita, y refere-se ao valor da variável (y). Por analogia com esta expressão, é usual distinguir numa variável estes dois aspectos distintos: L-Value: Referência ao local contendo o valor da variável (o L vem de left, ou seja do termo da esquerda de uma atribuição). R-Value: Valor da variável (o R vem de right, ou seja o termo da direita numa atribuição). Estes dois conceitos serão muito úteis para descrever o conceito de apontador. As variáveis de tipo apontador são variáveis que têm como valor um endereço de memória - i.e. uma referência a outro objecto do programa. Este tipo de dados não estava disponível nas primeiras linguagens, tal como o ALGOL60 e o COBOL. É no entanto indispensável em qualquer linguagem modena. O tipo de dados apontador é extensamente utili zado na linguagem C, nomeadamente através dos operadores * e &. Podemos relacionar estes operadores com as noções de l-value e r-value:

• &<l-value> : o endereço da variável <l-value>. • *<r-value> : a variável no endereço <r-value>.

3.3.2 Reserva Dinâmica de Memória Normalmente a memória para uma variável no programa é reservada na entrada num bloco. Esta reserva é efectuada de forma automática pelo código compilado. O tipo apontador é o único que permite efectuar esta reserva de memória directamente do código do programa (como veremos à frente existe uma zona de memória especial para esta reserva: o Heap). No PASCAL, Ada ou Java isto é feito utili zando a instrução new:

apontador = new integer; Esta instrução reserva espaço em memória para uma variável do tipo inteiro, colocando o endereço do espaço reservado na variável apontador. Em C a instrução equivalente, seria:

apontador = (int *) malloc(sizeof(int)); Podemos inclusivamente ter 2 apontadores para o mesmo endereço de memória. Se fizermos (utili zaremos a sintaxe do C, por clareza):

Universidade Aberta -18- 2000-12-14

outro = apontador;

tanto outro como apontador apontam para a mesma área de memória. Assim colocando um valor no R-Value de apontador:

*apontador = 3; tanto *outro como *apontador contêm o mesmo valor.

3.3.3 Dangling Pointers (apontadores pendentes) Um dos problemas com os apontadores é o chamado caso dos apontadores pendentes: apontadores que apontam para endereços de memória não reservada. Nos antigos sistema operativo da família DOS, por exemplo, a utili zação de um ponteiro não iniciali zado como no código que se segue poderia bloquear totalmente o computador.

main() { /* errado */ int *ptr; /* ptr é criado mas não está definido, aponta pois para qualquer local da memória. */ *ptr=5; /* erro: coloca 5 algures na memória: pode mesmo destruir o código do programa */ }

De facto, neste caso, o apontador para inteiro ptr recebe um valor, que será colocado algures na memória (pois o valor de ptr não é iniciali zado, e pode por isso referir qualquer posição na memória). No DOS, um apontador não iniciali zado poderia referênciar um endereço da memória da máquina contendo o código do próprio sistema operativo. Nesse caso seria provável que o computador ficasse bloqueado e fosse necessário fazer um reboot à máquina. Já em sistemas operativos com memória protegida, como por exemplo o sistema UNIX, o erro poderia ser menos grave (quando o sistema operativo detecta um acesso a uma área de memória protegida, i.e. que não pertence à àrea reservada para a execução do programa, este termina o programa com uma mensagem de erro apropriada, normalmente 'Segmentation fault.'). Tal já não aconteceria caso o apontador, por acaso, receba um valor sobre o strack ou heap. Nesse caso outra variável, ou posição da memória reservada, pode ser alterado. Este é o caso noutra situação onde se gera um dangling pointer:

int *ptr_a; foo() { int a; ptr_a=&a; }

Neste caso, ilustrado na figura 2 ptr_a recebe um apontador para uma variável local. Como acabamos de ver, nas linguagens modernas, as variáveis locais apenas existem dentro do código do bloco que as contém (neste caso a função foo). Assim sendo, ptr_a recebe um valor que apenas está reservado enquanto a função foo é executada. Se se colocar um valor em *ptr_a fora desta função é provável que se esteja a aceder ao valor de uma variável local qualquer no código (conforme veremos na secção 3.6, o espaço de reserva para as variáveis locais é normalmente o mesmo e está em constante reutili zação).

Universidade Aberta -19- 2000-12-14

Figura 2 : Quando a função foo termina, a àrea de memória que lhe estava reservada é libertada e ptr_a fica com um dangling pointer.

3.3.4 Garbage Colection A reserva dinâmica de memória têm outro problema. Imagine-se o seguinte caso: reserva-se memória dinâmica para uma dada informação, colocando-se esse endereço na variável apontador. Se, posteriormente, colocarmos outro endereço na variável apontador (e não tivéssemos entretanto copiado a referência a esse espaço para mais nenhuma variável) , o espaço reservado inicialmente deixaria de estar referênciado e não poderia voltar a ser utili zado pelo sistema. Se o programa, como normalmente acontece, funcionar em ciclo, ao fim de algum tempo, a memória para reserva dinâmica (o Heap) estará esgotada! É pois necessário limpar os endereços não referênciados regularmente. A esse processo chama-se Garbage Colection (recolha do li xo). Depende da linguagem a forma de resolver este problema. Linguagens como o PASCAL ou o C, deixam a garbadge colection à responsabili dade do programador, através, respectivamente das instruções dispose e free. A utili zação destas instruções requer no entanto cuidado especial pois se se libertar espaço reservado ainda referênciado está-se a criar um potencial dangling pointer. A linguagem JAVA , por exemplo, utili za um outro processo: enquanto o programa executa, existe um processo que continuamente verifica a área de memória dinâmica efectuando a garbage colection de forma automática: o garbage collector. Existem várias formas possíveis de efectuar a garbage colection, na linguagem JAVA , por exemplo ([CampioneWalrath98]) utili zou-se uma derivação da técnica mark-sweep collector (recolha por marcação e varrimento). Trata-se dumas das técnicas mais simples de garbage collection. Na sua forma original esta técnica funciona da seguinte forma:

1. Fase de marcação: Marcam-se todas as células que podem ser alcançadas através de apontadores. Uma boa analogia é apresentada em [Sethi89]: imagine-se que se espalha tinta através dos apontadores, para todas as células de memória em utili zação. A tinta espalha-se, de célula em célula até marcar todas as células em uso.

2. Fase de varrimento: Percorrer a memória toda, procurando células não marcadas. Sempre que uma célula não marcada é encontrada, esta é retornada para o espaço li vre do sistema.

Embora seja computacionalmente pesada, esta técnica liberta o programador da preocupação com a gestão da memória e torna assim o processo de programação mais simples e menos susceptível de erros com dangling pointers. Esta técnica é normalmente utili zada em linguagens funcionais ou lógicas como o LISP ou o PROLOG.

3.4 Expressões e Instruções Os principais componentes de uma linguagem imperativa são as declarações -que acabamos de estudar- as expressões e as instruções. O estudo das expressões e das instruções base é comum à maioria das linguagens modernas, incluindo o C++. Iremos pois focar o seu estudo no caso do C++. As expressões do C (e logo também do

Universidade Aberta -20- 2000-12-14

C++) incluem o conjunto de expressões típico de qualquer linguagem imperativa : • A noção de prioridade dos operadores. • Expressões matemáticas. • Expressões lógicas. • Expressões mistas. • Overloading de operadores (este último caso apenas presente no C++).

Os diversos tipos de instruções das diversas linguagens imperativas são também comuns ao C:

• Instruções e operadores de atribuição, múltiplos ou simples. • Instruções compostas (a noção de bloco). • Instruções de selecção (if e case). • Instruções iterativas (do while, do repeat e for).

O seu estudo foi efectuado, no caso do C, no módulo 2. Existe no entanto uma característica do C++ que não pode ser directamente implementada no C: o tratamento de excepções. Na próxima secção vamos estudar o seu funcionamento na linguagem JAVA e a forma como este mecanismo é utili zado no C++.

3.4.1 Tratamento de Excepções Durante a execução de um programa podem ocorrer eventos anormais, que tornam a continuação normal do programa não desejável ou mesmo impossível. É o caso frequente de erros como os devidos a: erros de hardware (erros no disco), operações matemáticas invalidas num dado domínio (divisões por zero, raiz quadrada de um número negativo ou número demasiado elevado para um dado tipo de dados - overflow), erros na entrada do utili zador ou um endereço inválido de um vector. A maioria das linguagens de programação, deixa o tratamento destes acontecimentos totalmente à responsabili dade do programador. Compete ao programador tratar estes erros como e quando acha necessário. Este tipo de abordagem pode facilmente conduzir a programas complexos e pode facilmente obscurecer a estrutura interna do programa. Outra abordagem é a de possibilit ar o tratamento de excepções como parte da linguagem. A primeira linguagem a fazer isto, foi como vimos, o PL/I. Mais recentemente linguagens como o Ada, C++ e o JAVA incluíram o tratamento de excepções de uma forma robusta. Quando um evento excepcional (como os que enumeramos acima) ocorre, diz-se que se levantou uma excepção. Nesse caso, a execução normal do programa é interrompida e o controle é transferido para uma parte especial do programa conhecida pelo exception handler (tratador de excepções). Normalmente, existem diferentes rotinas para tratamento das diferentes excepções. Um programa que trata as diferentes excepções que se podem levantar diz-se resistente à falha (fault-tolerant).

3.4.2 Tratamento das excepções em JAVA Após ter ocorrido uma excepção o sistema de execução do JAVA inicia uma busca do código para tratamento desse erro. Essa busca é iniciada no método (o equivalente a um procedimento quando trabalhando com objectos) onde ocorreu a excepção e, caso não haja ai qualquer código para tratamento da excepção, continua-se a busca, sucessivamente, no método que chamou o método actual. Desta forma, em JAVA , a excepção sobe como uma bolha por toda a pilha de métodos. Apenas no caso de não ser encontrada nenhuma rotina para tratamento da excepção, é que o programa termina (com uma mensagem de erro associada ao tipo de excepção detectada). Imagine-se pois que pretendiamos implementar a seguinte função (extraída de [CampioneWalrath98]):

lerFicheiro { abrirFicheiro(f); tamanho=obterTamanhoFicheiro(f); ptr=reservarMemoria(tamanho);

Universidade Aberta -21- 2000-12-14

lerParaMemória(ptr, tamanho, f); fecharFicheiro(f); }

à primeira vista, embora o código pareça relativamente simples, ignora todos os potenciais erros deste programa:

• que acontece se o ficheiro não pode ser aberto? • que acontece se não for possível determinar o tamanho do ficheiro. • que acontece se o comprimento do ficheiro não puder ser determinado? • que acontece nos casos em que a leitura falha? • que acontece quando o ficheiro não pode ser fechado?

Para respondermos a todas estas questões seria necessária a implementação de muito mais código. O programa acabaria por ficar em qualquer coisa como:

int lerFicheiro { int codigoErro=0; int tamanho; f=abrirFicheiro(s); if(ficheiroAberto(f)) { tamanho=tamanhoFicheiro(f); if(tamanhoFicheiroOK(tamanho)) { ptr=reservarMemoria(tamanho); if(reservaMemoriaOK(ptr)) { lerParaMemória(ptr, tamanho, f); if(not(lerParaMemóriaOK())) { codigoErro= - 1; } } else { codigoErro= - 2; } } else { codigoErro= - 3; } fecharFicheiro(f); if(not(ficheiroFechado(f)) && codigoErro==0) { codigoErro= - 4; } else { codigoErro=codigoErro + - 4; } } else { codigoErro= - 8; } return codigoErro; }

O que não só é complexo e com uma série de questões subtis, como também torna o código original totalmente ilegível! Este exemplo ilustra bem os problemas de que as linguagens sem capacidade de detecção de excepções sofrem. No entanto se nesta função utili zassemos as técnicas de controle de erros teriamos uma solução similar ao seguinte código:

lerFicheiro { try { abrirFicheiro(f);

Universidade Aberta -22- 2000-12-14

tamanho=obterTamanhoFicheiro(f); ptr=reservarMemoria(tamanho); lerParaMemória(ptr, tamanho, f); fecharFicheiro(f); } catch (abrirFicheiroFalhou) { ... ; } catch (fecharFicheiroFalhou) { ...; } catch (obterTamanhoFicheiroFalhou) { ...; } catch (reservaMemóriaFalhou) { ...; } catch (lerParaMemóriaFalhou) { ...; } }

Qualquer método (o equivalente a um procedimento quando trabalhando com objectos) JAVA pode levantar excepções ou erros, predefinidos ou definidos pelo utili zador, veja-se um exemplo com o método pop (que retira um elemento de uma pilha):

public Object pop() throws EmptyStackException { Object obj; if (size == 0) throw new EmptyStackException(); obj = objectAt(size - 1); setObjectAt(size - 1, null); size -- ; return ob j; }

Este método levanta a excepção EmptyStackException com a instrução throw, sempre que se tenta retirar um elemento de uma pilha vazia (a instrução new limita-se a criar o objecto EmptyStackException, pois em JAVA as excepções são também objectos). Em JAVA as excepções devem ser declaradas (o throws EmptyStackException após o cabeçalho do método). Apesar de poderem ser utili zadas para outros fins, é importante manter a utili zação de excepções para o tratamento de situações anormais ou de erro. De facto apenas em casos anormais ou de erro a utili zação de excepções aumenta a clareza do código. Em situações normais, a utili zação das instruções condicionais if e case é a que maior clareza trás ao código. Exercício: Utili ze as instruções throw e catch do C++ (consulte a ajuda on-line do C++ ) para implementar a função LerFicheiro em C++ , com tratamento de excepções.

3.4.3 A Instrução goto A utili zação da instrução goto é normalmente desencorajada desde a carta de Dijkstra para a conceituada revista Communications of ACM de titulo "Goto Statement Considered Harmful" (1968). A instrução goto é extremamente flexível, tendo sido extensamente utili zada nas primeiras versões da linguagem COBOL e FORTRAN. A utili zação excessiva de instrução goto, nomeadamente quando ligada a saltos condicionais (i.e. utili zando a instrução goto em conjunção com a instrução if) dá origem a código pouco legível, onde os erros são difíceis de locali zar e corrigir. Feli zmente a utili zação das instruções de ciclo (for e do, conjugada com as instruções break e continue) e de subprogramas torna a sua utili zação praticamente desnecessária. Na linguagem Modula-2, por exemplo, esta instrução já foi mesmo excluída.

Universidade Aberta -23- 2000-12-14

3.5 Sub-programas: Passagem de Parâmetros Operacionalmente os métodos de passagem de parâmetros podem ser divididos em: 1. passar informação para um subprograma. 2. receber informação de um subprograma. 3. passar informação que pode ser actuali zada antes de ser retornada. A linguagem Ada dá directamente conta destes 3 casos, respectivamente, com os argumentos do tipo:in, out e in-out. No entanto a maioria das linguagens não apresenta, directamente, estruturas que dêem conta de todos estes modos. Estes três modos podem ser implementados de diversas formas. Se pensarmos em termos de l-values e r-values, para uma chamada de subprograma P(a), podemos implementar:

• Uma chamada por valor (call by value). Neste caso passamos o r-value de a. • Uma chamada por referência (call -by-reference). Passamos o l-value de a. • Uma chamada por nome (call -by-name). Onde passamos o nome da variável (neste caso texto a) para

dentro do subprograma. • Chamada por valor-resultado: chamada por valor, sendo o l-value da variável actuali zado apenas no fim da

execução do subprograma. É semanticamente equivalente à passagem por referência. Operacionalmente podemos relacionar estes três modos com os modos operacionais anteriores. Utili zando a notação sintáctica do Ada:

• IN: Passagem por valor. • OUT: Passagem por resultado. • IN-OUT: passagem por valor-resultado, passagem por referência e passagem por nome.

O C utili za apenas a passagem por valor. No entanto devido à elevada flexibili dade desta linguagem é possível implementar igualmente a passagem por referência. Devido à existência do pré-processador esta linguagem suporta também a passagem por nome. Já no PASCAL e Modula-2, é possível utili zar tanto a passagem por valor (o caso por defeito), como a passagem por referência (quando se adiciona a directiva var antes do nome do procedimento). De seguida utili zaremos o exemplo da função swap para tentar ilustrar de forma clara as diferenças entre os vários métodos de passagem de parâmetros.

3.5.1 Passagem por Valor A passagem por valor é sem duvida a forma mais usual de passar parâmetros para uma função. Com a passagem por valor, os argumentos da função são copiados para variáveis locais à função. Pode-se mesmo pensar nesta variáveis como variáveis locais à função, criadas com o mesmo nome dos parâmetros da função e para as quais é copiado o valor do parâmetro. Este processo impede assim a alteração dos valores dos seus argumentos pela própria função. Imaginemos agora que pretendemos implementar a função swap que troca o conteúdo dos seus dois argumentos. Numa primeira abordagem, poder-se-ia pensar no seguinte programa C:

void swap_errado(int x, int y) { /* Mal implementado */ int z; z = x; x=y; y=z; }

No entanto, este código não funciona! De facto, como já foi referido, o C apresenta passagem por valor dos seus parâmetros: os valores dos seus argumentos são copiados para novas variáveis. Assim sendo, apenas as variáveis locais à função têm os valores trocados, mas esta troca perde-se quando o procedimento termina e estas variáveis são destruídas. Os argumentos de swap_errado mantém o seu valor. Talvez seja mais claro se considerarmos o que acontece após a chamada de swap_errado(a,b):

/*Criação das variáveis x, y e z */

Universidade Aberta -24- 2000-12-14

x = a; y = b; z = x; x = y; y =z; /*Destruição de x, y e z */

3.5.2 Passagem por Referência A passagem por referência pode ser feita em PASCAL (ou no Modula-2), utili zando a palavra reservada var . Apenas com a passagem por referência se torna possível a implementação da função swap, eis a implementação desta função em PASCAL:

procedure swap(var x : integer; var y : integer); var z : integer; begin z:=x; x:=y; y:=z end;

Para analisarmos o comportamento desta função, imagine-se um vector a, em que a[2]=99 a[99]=5 e i=2. Considere-se que swap é chamada com os seguintes argumentos:

swap(i, a[i]) O que acontece neste caso? Recordemos que na passagem por referência são os l-values das variáveis que são passados como argumentos. Assim, swap deverá efectuar os seguintes passos:

/*Criação das variáveis x, y e z */ x = l-value de i; y = l-value de a[i]; z = x; x = y; y =z; /*Destruição de x, y e z */

Desta vez, como são utili zados os l-value dos argumentos, estes são de facto alterados: x=y, é equivalente a i=a[i], ou seja i recebe 99 (i.e. o valor de a[2]). y=z, é equivalente a a[2]=i, ou seja a[2] recebe 2 (i.e. o valor inicial de i). Note-se que y recebe o valor de a[i] antes de ser efectuada qualquer alteração aos valores das variáveis! Só assim é possível efectuar a cópia correcta dos valores. De facto, a passagem por referência reduz-se a uma passagem por valor, utili zando apontadores para os argumentos a passar. É esta a razão porque o C não necessita de passagem por referência. De facto a função swap pode ser directamente implementada em C, utili zando apontadores:

void swap(int *px, int *py) { int z; z = *px; *px = *py; *py = z; }

As principais vantagens desta abordagem é a de estar mais próxima da implementação em termos de linguagem máquina e a de não necessitar de nenhuma estrutura sintáctica para o tratamento da passagem por referência. A principal desvantagem é o facto de os parâmetros da função, quando passados por referência, terem que ser des-referênciados, assim em vez da chamada swap(i, a[ i] ) do Pascal, em C para qualquer passagem por referência, será necessário escrever:

swap(&i, &(a[i])); (ou alternativamente swap(&i, a+i ) )

Apesar da mais críptico, este código acaba por ser mais fiel ao que de facto se passa ao nível da máquina. São os l-value e não os r-value que são passados como parâmetros.

Universidade Aberta -25- 2000-12-14

3.5.3 Passagem por Valor-resultado Usualmente, trata-se de uma abordagem equivalente à passagem por referência. Tal como na passagem por valor, o argumento é também copiado para variáveis locais ao subprograma (fase copy in). No entanto o valor de retorno pode não se perde, pois ele é de novo copiado para as variáveis argumento (fase copy out):

/*Criação das vari áveis x, y e z */ x = a; y = b; z = x; x = y; y =z; /* Conclusão do subprograma */ a=x; b=y; /* Destruição de x, y e z */

Em rigor, na maioria das linguagens, há casos em que este processo pode dar valores diferentes, considere-se o programa em pseudo-código Pascal:

program ... procedure foo(inout x:integer, inout y : integer);

begin i := y

end;

begin i:=2; A[i]:=99; foo(i, A[i]); end.

Neste caso, i, seria reescrito 2 vezes: a primeira dentro do procedimento foo, ficando i com o valor 99, e a última, mais indirecta, na saída do procedimento, voltando i ao valor 2. Na linguagem Ada, o compilador considera este tipo de construções ilegal, podendo o compilador optar por utili zar a passagem por referência ou por valor-resultado, pois ambas são semanticamente equivalentes.

3.5.4 Passagem por Nome: Expansão de Macros em C Num programa C é igualmente possível uma outra forma de passagem de argumentos. De facto, antes de ser compilado, qualquer programa C é pré-processado por um processador de macros, destinado a suportar extensões à linguagem, substituição de constantes e inclusão de ficheiros. Em C, todos os comandos do pré-processador são iniciados pelo caracter #. Uma das funções deste pré-processador é a de efectuar a substituição literal de todas as macros pelo código que lhes está associado. A vantagem deste tipo de substituição é a sua elevadíssima eficiência: de facto o código do programa é alterado em tempo de compilação, não sendo chamada qualquer função em tempo de execução. O mecanismo da passagem de parâmetros para uma macro é no entanto extremamente condicionado. Imagine-se que tentávamos implementar o procedimento swap utili zando uma macro:

#define SEMI_SWAP(x,y,z) \ z=x; \ x=y; \ y=z;

Que funciona na maioria dos casos (o terceiro parâmetro deverá receber uma variável que será utili zada com auxili ar). No entanto, se tentássemos utili zar esta macro para a troca de valores entre i e a[i], anteriormente apresentada, os resultados seriam totalmente diferentes. Substituindo os valores na chamada SEMI_SWAP(i,a[i], aux), vemos porquê (relembre-se que, inicialmente i=2 e a[2]=99):

Universidade Aberta -26- 2000-12-14

aux = i; /* aux=2 */ i=a[i]; /* i=99 */ a[i]=aux; /* a[i] é agora a[99] e não a[2] como pretendido */

Isto é, o segundo parâmetro, a[i], apresenta um naming confict com o primeiro parâmetro, i. Ao alterarmos i, perdemos o l-value de a[i]! Devido a este tipo de problemas, a maioria das linguagens não possibilit a a passagem por nome. O ALGOL60, tentou ser uma excepção a esta regra. Para tal enunciou um principio que generali za o problema atrás descrito: Conflito de nomes: Um conflito de nomes ocorre durante a substituição do texto T em S se um nome não local em T é alterado sobre o escopo da associação em S. Para resolução deste tipo de conflitos o ALGOL60, sempre que detectasse qualquer variável em conflito com este principio, recorria à substituição do nome dessa variável em todos os locais do código que pudessem ser problemáticos. No entanto, devido à complexidade de implementação de tal tarefa e à maior clareza da passagem por nome/referência, a passagem por nome deixou de ser útil e já não é utili zada na maioria das linguagens de programação dos dias de hoje. O pré-processador de macros do C, é das poucas excepções a esta regra. Na prática, no C optou-se por ignorar o problema dos conflitos de nome: um programa implementado em C, utili zando a macro SEMI_SWAP, com a chamada apresentada, produzirá de facto o resultado (inesperado) aqui apresentado. É da responsabili dade do programador utili zar as macros por forma a evitar este tipo de conflitos. Na prática, as macros do C apenas foram introduzidas devido às necessidades de eficiência e flexibili dade de alguns programas escritos em C. As macros com argumentos do C apenas devem ser utili zadas, tendo os devidos cuidados, em código em que a sobrecarga da chamada de um procedimento seja de facto grande.

3.6 Gestão de Memória Normalmente, as linguagens de programação modernas são implementadas reservando uma área de memória para os dados do programa. Existem duas estratégias principais para reserva desta memória: a estática e a dos blocos dinâmicos.

3.6.1 Reserva Estática Foi um processo utili zado na linguagem FORTRAN. Neste método a memória para os dados é reservada de forma estática para cada elemento. Este processo permite determinar em tempo de compilação quais os endereços de memória efectivos para cada variável. Infeli zmente, e apesar da sua simplicidade este modelo não é suficiente para as necessidades das linguagens modernas: o tamanho de todos os objectos tem de ser conhecido em tempo de compilação e nenhuma variável pode ocorrer mais de uma vez, como no caso de uma função recursiva, p.ex., que efectue o cálculo dos números de Fibonacci quando o argumento é um número positi vo e que retorne –1 quando o argumento é um número negativo:

int Fib(int n) { if(n<0) return –1; if(n<=2) return 1; return Fib(n - 1)+Fib(n - 2) }

Neste caso se a reserva de memória fosse estática, o programa limitar-se-ia a decrementar n no primeiro termo da série, sendo o valor utili zado no segundo termo influenciado pelas chamadas no primeiro termo, a seguinte sequência de chamadas sucessivas da função Fib, com o valor de n descriminado, ilustra o que se passaria, primeiro utili zando memória dinâmica e depois memória estática:

Universidade Aberta -27- 2000-12-14

n=2

Fib(3-1)

n=1

Fib(3-2)

+=1

n=3

=1

Fib(4-1)

n=2

Fib(4-2)

+

=2

=1

Fib(4) =3n=4

Memória dinâmica:

n=2

Fib(3-1)

n=0

Fib(2-2)

+=1

n=3

=1

Fib(4-1)

n=-2

Fib(0-2)

+

=2

=-1

Fib(4) =1n=4

Memória estática:

Consideremos então o caso de memória estática: quando se chama a função Fib, o valor de n é actuali zado. No primeiro termo da expressão n vai recebendo valores sucessivamente menores. Só quando n é menor ou igual a 2 é que a função retorna. No entanto não é agora possível recuperar o valor anterior de n=3, para o utili zar no segundo termo! Assim o último valor de que o computador se lembra, n=2, é utili zado.

3.6.2 Reserva Dinâmica de Blocos Tal como na gestão estática, o bloco principal do programa tem as suas variáveis reservadas no inicio da execução do programa. No entanto a associação das variáveis dum subprograma (p.ex. função ou procedimento) a um endereço de memória só ocorre quando se entra nesse subprograma. Este tipo de associação é normalmente designada por stack storage management. De seguida vamos ilustrar como se processa a gestão dinâmica de memória num programa C.

Numa linguagem com gestão dinâmica de memória uma variável declarada num dado bloco de código é local à execução desse bloco de código. Assim o compilador utili za normalmente uma pilha (ou stack) para armazenar as variáveis locais ao bloco de código em execução. Os dados necessários para activar um subprograma são guardados num registo chamado activation record ou frame. Estas frames são guardadas numa pilha: o sack frame, criado no topo do stack quando o controle entra no bloco de código. Quando o controle do programa sai de dentro desse bloco de código, já não há necessidade de utili zar essa variável, e o activation record é removido do topo do stack frame, podendo os l-value das variáveis locais desse módulo serem reutili zados e atribuídos a outras variáveis locais desse ou doutro bloco.

3.6.3 Gestão de Memória nas Linguagens da Família do C

Universidade Aberta -28- 2000-12-14

O que se segue foi extraído de [Sethi86]. Esta secção serve para exempli ficar como os conceitos de que acabamos de falar são implementados na linguagem C (e por consequência em C++). Note-se que o esquema de gestão de memória no C é um pouco mais simples que em linguagens como por exemplo o Pascal onde os procedimentos podem estar aninhados (nested) uns nos outros. A figura abaixo ilustra como um programa C (com vários blocos) organiza e gere a memória de um computador.

Figura 3 : Gestão da memória num programa C/C++.

Existe um registo do processador, chamado de program counter que mantém o controle da instrução que está correntemente a ser executada pelo computador. O código de um programa C, que não pode ser alterado durante a execução do programa, é representado pela caixa "código" na esquerda da figura. Os dados globais são guardados em memória reservada estaticamente (representada no canto superior direito da figura pela caixa "Dados Globais Estáticos"). Por sua vez os dados locais são guardados no stack frame (caixa imediatamente abaixo), que como vimos aumenta e diminui de tamanho consoante necessário. De igual forma, no outro extremo da memória existe uma área chamada de heap para armazenar os dados reservados pelos procedimentos calloc e malloc, utili zados na reserva dinâmica de memória. Refira-se que, durante a execução do programa, a memória li vre para o programa pode esgotar-se tanto quando se reserva memória dinamicamente (p.ex., por não se chamar a instrução free), como por se declararem demasiadas variáveis locais (ou vectores demasiado grandes). Como deve ser neste momento obvio, a reserva de demasiado espaço para variáveis locais pode exigir demasiada memória, especialmente em procedimentos recursivos. Estes problemas de memória eram particularmente incómodos nas primeiras versões do Borland C, pois (para aumentar a eficiência do acesso a memória, utili zando apenas 16 Bit para os L-Value) este programa limitava o espaço de memória dinâmica apenas a 64k no modo normal. Apesar deste valor poder ser multipli cado para 640k com a configuração adequada do compilador, tal era ainda insuficiente para a maioria das apli cações mais exigentes.

3.6.4 O Stack-frame dum Programa C Para completar a descrição da gestão de memória dinâmica no C resta apenas explicar como o compilador de C organiza o activation-record para cada chamada de uma função C. Esta forma de organização da memória faz parte da definição da linguagem C e contribuiu definiti vamente para que, na mesma plataforma, se possam juntar procedimentos escritos nas mais diversas linguagens. Num ambiente Unix, por exemplo, todas as principais linguagens da actualidade ou suportam a chamada de uma função escrita e compilada em C ou podem ter os seus subprogramas chamados de dentro de um programa em C. Considere a seguinte figura:

Universidade Aberta -29- 2000-12-14

Figura 4 : Gestão do stack-frame numa chamada de procedimento em C/C++.

As seguintes acções ocorrem quando uma função ou procedimento C é chamado: 1. Atendendo a que o C utili za (sempre) passagem de parâmetros por valor, o programa principal ou a função

chamante (i.e. o programa ou subprograma que chama a função corrente), avalia os parâmetros de chamada, e coloca os resultados (por ordem inversa) no inicio do Stack.

2. A função chamante coloca de seguida no stack a informação necessária para restaurar a sua execução (quadrado estado do programa), uma vez que a função chamada termine (inclui o endereço do program counter para onde a função chamada deve retornar a execução).

3. A função chamada reserva espaço para as suas variáveis locais. É ainda reservado mais algum espaço na área de trabalho, para algumas variáveis de trabalho, de que o código compilado necessita (p.ex. para guardar cálculos intermédios na avaliação de expressões complexas).

4. O código da função chamada é então executado. Note-se que a função chamada pode funcionar também como função chamante de outras funções (ou dela própria no caso de uma função recursiva). Nesse caso o stack frame é utili zado para guardar os parâmetros e activação das novas chamadas (quadrado parâmetros de chamada). Compete à função chamante remover esses parametros quando a função chamada retornou. Assim, após o retorno à função corrente, o stack frame volta a ter o tamanho inicial.

5. O controle retorna à função chamante. Um valor de retorno (se existir) é colocado no local apropriado da função chamante e a informação sobre o estado do programa é retirada do stack para restaurar os registos do processador para o estado de partida antes da chamada. O controle retorna ao endereço do program counter, que tinha sido previamente guardado.

3.7 Exercícios Na página web da cadeira poderá encontrar a li sta de exercícios propostos para este módulo de estudo.

Universidade Aberta -30- 2000-12-14

Universidade Aberta -31- 2000-12-14

Módulo de Estudo 4 : Declarações, Definições e Classes e m C++ Enquanto na secção anterior estudamos a forma e mecanismos de implementação das linguagens de programação em geral. Neste módulo de estudo propõe-se a concretização dos conceitos apresentados numa linguagem concreta: o C++. Um particular detalhe será dado aos paradigmas de Abstract Data Types e Object Oriented Programming que serão introduzidos no capítulo 5 de [RodriguesPereiraSousa98].

4.1 Objectivos Os objectivos deste módulo de estudo são apresentados nas páginas 145 e 235 de [RodriguesPereiraSousa98].

4.2 Material de Estudo O material de estudo teórico é composto pelos capítulos 4 e 5 (páginas 145 a 338) de [RodriguesPereiraSousa98]. Relembra-se que o CD-ROM que acompanha o li vro contém um conjunto de acetatos que o poderão auxili ar no seu estudo. Como complemento ao estudo teórico, os alunos deverão utili zar o compilador de C++ para introduzirem, executarem e analisarem (introduzindo se necessário pequenas alterações no código), os exemplos apresentados ao longo do texto.

4.3 Exercícios No fim do estudo do capítulo 4, o aluno deve ser capaz de resolver a totalidade dos exercícios propostos nas páginas 228 a 234 de [RodriguesPereiraSousa98]. Em particular o aluno deverá resolver os exercícios 1 a 7, 12 e 14. No fim do estudo do capítulo 5, o aluno deve ser capaz de resolver a totalidade dos exercícios propostos nas páginas 335 a 338 de [RodriguesPereiraSousa98]. Em particular o aluno deverá resolver os exercícios 1, 2, 7 e 11. Por fim crie uma função main() que permita o teste dos novos métodos criados.

Universidade Aberta -32- 2000-12-14

Universidade Aberta -33- 2000-12-14

Módulo de Estudo 5 : Introdu ção à Programação em Lóg ica No último módulo de estudo da cadeira iremos abordar o estudo da principal li nguagem para a programação em lógica: o PROLOG.

5.1 Objectivos Os principais objectivos deste módulo são:

• Compreender programas escritos em estilo declarativo em PROLOG. • Compreender a semântica procedimental e o conceito de unificação. • Compreender que num programa PROLOG dados e código são equivalentes. Podendo mesmo um

programa PROLOG controlar-se a si próprio, gerindo a sua base de dados, através de meta-regras. • Saber quando um programa PROLOG simples tem múltiplas soluções. • Saber ler e escrever regras básicas para interrogar uma base de dados PROLOG escrita em estilo

declarativo. • Saber ler e analisar programas escritos em PROLOG. • Adicionar regras básicas a programas PROLOG escritos em estilo declarativo. • Introduzir algumas Queries num interpretador PROLOG.

5.2 Material de Estudo O material de estudo é composto pela secção 5 da II parte do 1º li vro adoptado (páginas 93 a 106). Como complemento ao estudo teórico, os alunos deverão utili zar um interpretador PROLOG, fornecido na página web da cadeira, para introduzirem executarem e analisarem (introduzindo se necessário pequenas alterações no código), os exemplos apresentados no texto e outros exemplos apresentados na página web da cadeira.

5.3 Exercícios Na página web da cadeira poderá encontrar a li sta de exercícios propostos para este módulo de estudo.

Universidade Aberta -34- 2000-12-14

Universidade Aberta -35- 2000-12-14

Referências

Coelho84 Problemas e Linguagens de Programação por Helder Coelho, LNEC, 1984.

RodriguesPereiraSousa98 Programação em C++ , conceitos básicos e algoritmos por Pimenta Rodrigues, Pedro Pereira e Manuela Sousa, FCA- Editora de Informática. 1998. ISBN 972-722-038-X.

WilsonClark93 Comparative Programming Languages, 2nd Edition. Leslie B. Wilson e Robert G.Clark, Addison-Wesley, 1993. ISBN 0-201-56885-3.

Sethi89 Programming Languages, Concepts and Constructs, Ravi Sethi, 1st Edition, Addisson-Wesley, September 1989. ISBN 0-201-10365-6.

Martins97 Introdução à Programação usando o PASCAL, J. Pavão Martins, Mc-Graw Hill , 1994. ISBN: 972-9241-79-7.

CampioneWalrath98 The Java Tutorial Second Edition, Mary Campione e Kathy Walrath, Addisson-Wesley, Março 1998. ISBN 0201310074. http://java.sun.com/docs/books/tutorial.