Você está na página 1de 24

Linguagens de Programação 1 - Notas de Aula

Aviso: Essas notas de aula tem por finalidade orientar o professor no encaminhamento das aulas, a fim
de não deixar assuntos fora da ordem planejada. A leitura deste material não reduz a necessidade de
leitura da bibliografia recomendada. Última atualização em 23/02/11.

1. Paradigmas de Linguagens de Programação


Existem muitas linguagens de programação, todas elas com características diferentes. Por vezes,
muito diferentes. As linguagens no entanto, adotam técnicas comuns de programação,
desenvolvidas de forma independente de uma linguagem conforme a área de Linguagens de
Programação evolui.
As linguagens de programação podem ser classificadas de muitas formas diferentes. Umas das
formas de classificação é quanto ao paradigma de programação. Mas o que é um “paradigma”?
A palavra paradigma pode significar muitas coisas. No contexto científico, costuma ser usada para
um conjunto de teorias, padrões e métodos que juntos representam uma forma de organizar o
conhecimento, ou seja, uma forma de ver o mundo [Budd 91]. No contexto de linguagens de
programação, podemos dizer de maneira bem simples que paradigma é um estilo. Porém estamos
usando uma conotação forte, para serem chamados de paradigmas de programação, estilos precisam
ser suficientemente diferentes e completos para serem considerados uma forma de ver o mundo.
Autores diferentes classificam as linguagens de programação de formas diferentes. Apesar disso, é
geralmente aceito que existem dois tipos principais de programação: (a) programação imperativa
(ou procedural), de acordo com a qual os programas são construídos por meio de uma sequência de
ordens - o programador escreve um procedimento que leva à solução de um problema; (b)
programação declarativa (ou descritiva), de acordo com a qual os programas são construídos
simplesmente pela descrição da solução de um problema - cabe ao compilador encontrar uma
sequência de instruções que produz a solução declarada.
Na programação declarativa, existem duas formas principais de se descrever soluções: (a) através de
funções (conceito matemático de associação entre elementos de um conjunto domínio para um
conjunto imagem) e (b) através de predicados (elementos sintáticos que permitem expressar
relações entre elementos de conjuntos). Vale lembrar que uma função é um tipo especial de relação
de associação.
Assim, dadas essas duas categorias gerais distintas e o fato de que em uma delas, existem duas
formas muito diferentes de descrever a solução de um problema, o autor deste texto considera que
são três os paradigmas de programação:
• programação imperativa (ou procedural) - em que programas são implementações de
algoritmos (sequência de instruções que levam à solução de um problema);
• programação funcional - em que programas são implementações de funções e
• programação lógica - em programas são implementações de predicados (relações entre
elementos de conjuntos diversos).
É comum encontrar autores que classificam a Programação Orientada a Objetos como sendo um
paradigma de programação. Essa classificação considera que o projeto de programas orientados a
objetos segue conceitos de destaque suficiente para que esse estilo de programação seja classificado
à parte da programação imperativa.
Este texto se concentra na programação imperativa, apesar de apresentar conceitos que são
independentes do paradigma de programação. Alguns conceitos em particular são melhor
exemplificados através de suas implementações em linguagens funcionais como (LISP, ML,
Notas de Aula de GCC105 - BCC e BSI - UFLA

Haskell, etc.) e nesses casos daremos preferência a exemplos nessas linguagens.

2. Conceitos de Linguagens de Programação

2.1 Tipos de Dados


Algumas linguagens não tem o tipo de dado que você precisa, pode ser necessário implementá-lo
(ex.: implementar listas ou árvores usando vetores).
Algol68 permitia a declaração de novos tipo, um avanço significativo.
Um tipo primitivo de dados é aquele que não pode ser decomposto em partes mais simples [Watt,
89]. Para tal classificação, considera-se a linguagem propriamente dita (e não suas bibliotecas).

2.1.1 Inteiros
Diferentes capacidades. Com ou sem sinal. Complemento de dois X Complemento de um.
Complemento de dois requer circuito simples. Complemento de um permite duas representações
para o zero.

2.1.2 Ponto Flutuante


Objetivo de manter a maior quantidade de bits significativos. Mantissa e expoente.
A norma IEEE 754 estabelece 23 bits de mantissa e 8 de expoente para precisão simples; 52 bits de
mantissa e 11 bits de expoente para precisão dupla.

2.1.3 Decimais
Ponto fixo, útil em aplicações comerciais. Algumas máquinas tinham hardware próprio para esse
tipo e representavam números como strings de bits, desperdiçando um pouco de espaço. Hoje em
dia, costuma-se usar emulação desse tipo no software.

2.1.4 Booleanos
Um único bit não é normalmente endereçável na memória.
Tipos booleanos aumentam a legibilidade da linguagem (ver exemplo na seção de expressões
booleanas).

2.1.5 Caracteres
Tamanhos fixos e variáveis.

2.1.6 Ponteiros
Proporcionam flexibilidade para outros tipos.

2.1.7 Strings
Quando são tipos primitivos, strings costumam vir acompanhas de diversos operadores para

prof. Bruno Schneider


Notas de Aula de GCC105 - BCC e BSI - UFLA

comparar, extrair substring, concatenar, etc.


Para manter o tamanho variável num espaço estático de memória, é preciso algum tipo de controle.
FORTRAN usa strings sempre cheias. C usa o terminador zero. Pascal usa o tamanho no primeiro
elemento do vetor (problemas de limite para o tamanho). Strings podem ser dinâmicas.
Por serem elementos compostos, as operações podem ser complicadas (atribuir, comparar, etc.).
As linguagens que fornecem formas de descrever padrões de strings se destacam na criação de
programas que precisam manipular texto. Destas, SNOBOL (em especial a versão 4) tem
importância especial por entender os padrões como tipos de primeira classe (que podem ser
manipulados como qualquer outro tipo primitivo). Das linguagens mais modernas, Perl, é uma das
mais lembradas por seus recursos de casamento de padrões. A representação por listas pode facilitar
a manipulação.
Exemplo: teste em Perl para ver se uma string contém uma data:
if $linha =~ (0[1-9]|[12][0-9]|3[01]) / (0[1-9]|1[012]) / ([12][0-9]{3})
Algumas linguagens permitem interpolação de strings, o que facilita a construção de strings a partir
de diversos valores de tipos diferentes.
Strings tem um papel importante na segurança de computadores (buffer overflow).

2.1.8 Enumerados
Amentam a legibilidade do programa. Facilitam a detecção de erros de faixa/limite.
Usa constantes simbólicas para os valores possíveis. Temos problemas se uma mesma constante
aparece em mais de um tipo.

2.1.9 Subfaixas
Aumentam a legibilidade ao tornar explícitos limites de um tipo numérico. Erros podem ser
detectados em tempo de compilação. Herdam as operações do tipo “pai”.

2.1.10 Vetores
Agregado homogêneo de valores. Os valores ficam em posições consecutivas na memória.
Algumas linguagens usam parênteses para os índices, outras usam colchetes. Uso de parênteses
deixa a sintaxe semelhante à uma chamada de função.
Algumas linguagens permitem o uso de enumerados ou subfaixas como índices.
Algumas linguagens determinam o valor mínimo do índice.
Algumas linguagens não verificam os limites dos índices.
Alocação estática ou dinâmica.

2.1.11 Vetores associativos (hashes)


São estruturas chave-valor. Aumento da capacidade pode ser muito custoso. Podem ser
implementados por árvores.

prof. Bruno Schneider


Notas de Aula de GCC105 - BCC e BSI - UFLA

2.1.12 Registros
Agregado heterogêneo. Elementos referenciados por nomes. A declaração de classes supre a
necessidade dos registros.

2.1.13 Uniões (Registros variantes)


Podem ser um dentre vários tipos diferentes. Trazem problemas para a verificação de tipos.
Problemas podem ser mitigados usando marcas (tags) que identificam o tipo usado (ex.: Haskell).

2.1.14 Listas
É uma estrutura de dados homogênea e recursiva. Requer que a estrutura seja percorrida para
acessar um valor. Por ser segmentada, aumenta e diminui com facilidade.

2.1.15 Conjuntos
Linguagens mais antigas podem ter limitações severas com relação à quantidade de elementos do
conjunto.
Fornecem apenas facilidade na implementação (ex.: verificar se uma letra é uma vogal).

2.2 Variáveis: nomes e vinculação


Variáveis podem ser entendidas como um conjunto de atributos (nome, endereço, valor, tipo, tempo
de vida e escopo). Nem todos os atributos são definidos no mesmo momento. A associação de um
atributo ao seu valor é uma vinculação.
As variáveis aumentam a legibilidade dos programas (em relação à endereços) e resolvem o
problema dos endereços absolutos.

2.2.1 Nomes
Além das variáveis, subprogramas, parâmetros, instruções (pontos no sequência de instruções de
um programa), comandos e outros elementos podem receber nomes. Nem toda variável precisa ter
nome.
Algumas linguagens limitam o tamanho permitido para os nomes. Compiladores também costumam
impor limites.
Algumas linguagens fazem distinção entre maiúsculas e minúsculas, o que é geralmente
considerado uma desvantagem.
Palavras reservadas: o significado é o mesmo em qualquer contexto.
Palavras-chave: o significado é especial em algum contexto.
Muitas vezes, palavras especiais são apenas “pré-definidas”, podendo ser redefinidas pelo
programador.
O mesmo nome pode estar associado a diferentes endereços (ex.: variáveis locais com mesmo nome
em dois subprogramas).
Nomes diferentes podem estar associados ao mesmo endereço (aliases). Aliases podem ser criados
de forma implícita (ex.: registros variantes) ou aparecer como efeito colateral do uso de ponteiros.

prof. Bruno Schneider


Notas de Aula de GCC105 - BCC e BSI - UFLA

Para reduzir os problemas da sobreposição de nomes, algumas linguagens usam “espaços de


nomes” (namespaces). Eles podem ser entendidos como mapeamentos entre nomes e variáveis.

2.2.2 Vinculação
Uma vinculação é estática se for se a mesma durante todo o tempo de execução. Vinculação
dinâmica é aquela que pode ser alterada durante a execução.
Vinculações podem acontecer no tempo de: projeto da linguagem, implementação da linguagem,
compilação, ligação, carregamento ou execução.

2.2.2.1 Vinculação de tipos


Pode ser explícita ou implícita. No segundo caso, é comum que a vinculação possa ser alterada a
qualquer hora, o que torna inviável a detecção de alguns erros em tempo de compilação. Alguns
desses problemas são amenizados exigindo caracteres especiais que indicam tipos (como em Perl).
Algumas linguagens tem vinculação estática de tipos, mas fazem coerção automática (em muitos
casos), gerando problemas parecidos de detecção de erros (ex.: C).
A vinculação dinâmica facilita a criação do compilador, que não precisa conhecer o tipo de uma
variável durante o seu uso, ela é mais comum nas linguagens interpretadas, nas linguagens antigas e
nas linguagens para web. Ela tem a vantagem de proporcionar um tipo de polimorfismo.
Compiladores simples ainda são importantes em situações como programas para web.
A vinculação estática proporciona maior velocidade de execução por dois motivos: (a) não é preciso
gerar código para sistema de tipos, pois o compilador já verificou a validade de todas as operações e
já determinou quais são as operações usadas (ver sobrecarga de operadores) e (b) as informações
sobre os tipos permitem ao compilador fazer otimizações no código gerado.

2.2.2.2 Inferência de Tipo


A partir de tipos de parâmetros e/ou operadores, é possível inferir tipos automaticamente,
permitindo a vinculação estática sem a necessidade de declarações.

2.2.2.3 Armazenamento (endereço) e Tempo de Vida


O tempo de vida de uma variável é o tempo durante o qual ela está vinculada à uma célula de
armazenamento (um endereço).
Em função do tempo de vida, uma variável pode ser classificada como:
• estática: a vinculação existe durante toda a execução do programa e não muda; tem
endereçamento direto (mais rápido); não existe overhead para alocar e desalocar; permitem
criar variáveis “sensíveis à história” podem ser um desperdício de espaço se não forem
usadas ao longo de toda a execução; não podem ser usadas em subprogramas recursivos.
• stack-dinâmica: a vinculação é criada quando a execução atinge a declaração da variável e
deixa de existir quando o bloco de código termina sua execução; as vinculação de tipo são
estáticas; funciona como uma forma de compartilhar memória entre os vários subprogramas.
• heap-dinâmica explícita: são criadas (sem nome) por operadores e funções de alocação de
memória e são acessadas por meio de referências (perigoso); tem vinculação estática de tipo;

prof. Bruno Schneider


Notas de Aula de GCC105 - BCC e BSI - UFLA

apresentam o overhead do acesso indireto. Ex.: objetos em Java.


• heap-dinâmica implícita: são vinculadas ao armazenamento e ao tipo durante uma
atribuição; são muito flexíveis; reduzem a capacidade do compilador de encontrar erros.
O gerenciamento do heap pode ser feito por algoritmos de garbage collection.
O tempo de vida de uma variável geralmente é tempo de execução de um bloco. Várias linguagens
oferecem formas de vinculação estática ao armazenamento, o que permite a criação de um tipo de
memória em subprogramas.
Nem sempre uma variável vinculada a um armazenamento é acessível, o que é desejável, seguindo
o princípio do escopo mínimo.
Vinculação dinâmica ao armazenamento é uma propriedade que conflita com instruções tipo “goto”.

2.2.3 Verificação de tipos


É a verificação entre tipos de operadores (ou funções) e seus operandos. Os tipos devem ser iguais
ou compatíveis (quando passam por coerção automática).
Pode ser estática (em tempo de compilação) ou dinâmica (tempo de execução). A forma dinâmica
exige que se mantenha informação sobre os tipos em tempo de execução.
Existe uma tendência a se preferir que haja verificação de tipos e que ela seja feita em tempo de
compilação (perda de flexibilidade para o programador).

2.2.4 Tipificação Forte


É a característica da linguagem que permite detectar qualquer erro de tipos.
Em geral sempre existe algum erro de tipo que não pode ser verificado, porém existem linguagens
com tipificação notadamente “mais forte” do que outras. Essas linguagens são tidas como “mais
confiáveis”.

2.2.5 Compatibilidade de Tipos


Alguns tipos deveriam ser passíveis de serem usados em lugar de outros sem que haja necessidade
de coerção. Isso pode feito de duas formas:
• compatibilidade de nome: duas variáveis tem o mesmo tipo se estão na mesma declaração
ou são declaradas com tipos de mesmo nome.
• compatibilidade de estrutura: duas variáveis tem o mesmo tipo se têm estruturas idênticas.
A primeira forma é muito restritiva e com frequência diferencia tipos que o programador não queria
diferenciar. Exige que tipos sejam declarados globalmente, podendo causar problemas com
bibliotecas.
A segunda forma traz problemas de indireção pois tipos podem ser formados de outros tipos e
podem ser recursivos. Ela torna impossível a diferenciação de tipos com a mesma estrutura (o que
pode ser desejável)
Na prática as linguagens usam uma mistura dos dois tipos pois nenhum dos dois produz os
resultados tidos como desejáveis em alguns casos. Na orientação a objetos a compatibilidade de
tipos está fortemente ligada ao conceito de herança.

prof. Bruno Schneider


Notas de Aula de GCC105 - BCC e BSI - UFLA

2.2.6 Escopo
O escopo de uma variável é o trecho de código no qual ela pode ser referenciada.
Uma variável é local à um bloco de código se foi declarada nele. Se não foi, mas é visível, é dita
externa.
É desejável restringir tanto quanto possível o escopo das variáveis e procedimentos, para que não
haja múltiplos nomes disponíveis à um bloco de código sem necessidade.

2.2.6.1 Escopo Estático


No escopo estático, é a estrutural textual do programa que determina o escopo. As variáveis
declaradas num bloco podem ser usadas em seus sub-blocos.

2.2.6.2 Escopo Dinâmico


No escopo dinâmico, é a chamada dos subprogramas que determina a visibilidade. As variáveis do
subprograma chamador são visíveis na execução do subprograma chamado.
O escopo dinâmico torna programas menos legíveis pois o ambiente de referenciamento muda em
tempo de execução, mais lentos pois exige que a resolução dos nomes seja feita em tempo de
execução e impossibilita a verificação de tipo, em tempo de compilação, para variáveis não locais.
Por outro lado, pode usado para proporcionar uma forma de polimorfismo e pode ser usado como
forma de passagem flexível de informações para subprogramas. O custo dessa flexibilidade é alto
em função das desvantagens já mencionadas e também porque funciona como um incentivo ao uso
de variáveis externas em oposição aos parâmetros.

2.2.7 Inicialização
Inicialização é a vinculação ao valor no momento da vinculação ao armazenamento. Variáveis
estáticas em termos de armazenamento precisam ser vinculadas a um valor antes da execução.
Nesse caso, pode não ser possível usar uma expressão para a inicialização.
Nem toda linguagem oferece recursos de inicialização (ex.: Pascal).

2.3 Avaliação de expressões


Uma linguagem deveria determinar precedência de operadores, regras para não coincidência de
tipos e para avaliação em curto-circuito.

2.3.1 Expressões aritméticas


Consistem em operadores, operandos, parênteses e funções. Operadores podem lidar com um ou
mais operandos e podem ser sobrecarregados em algumas linguagens.
Quase sempre as linguagens usam parênteses para fazer associação explícita.

2.3.1.1 Ordem de avaliação de operadores


É parcialmente determinada pela precedência dos operadores e parcialmente pela associatividade na
expressão; seguindo os conceitos da matemática.

prof. Bruno Schneider


Notas de Aula de GCC105 - BCC e BSI - UFLA

Um operador pode ter associatividade à direita (ex.: x = z = 1;) ou à esquerda (à esquerda é mais
comum). Um operador pode ser não associativo, exigindo a presença de parênteses. Quando os
operadores são de mesma precedência, a associatividade à esquerda diz que o operador da esquerda
Por serem aproximações das expressões matemáticas, os limites dos tipos podem tornar uma
expressão não-associativa (ex.: soma com números muito grades e muito pequenos).
As linguagens C, C++ e Java oferecem um operador condicional ternário (?:).
Algumas ordem podem ser mais rápidas que outras, pode ser interessante deixar que o compilador
escolha a ordem.

2.3.1.2 Ordem de avaliação dos operandos


A ordem de avaliação dos operandos pode influenciar o resultado (ex.: funções com efeitos
colaterais).
A algumas ordens de avaliações podem ser mais rápidas que outras.
Algumas linguagens deixam a ordem a critério do compilador e existem que o programador se
preocupe em evitar os casos em que a ordem é importante (ex.: Pascal e Ada) enquanto outras
determinam uma ordem (ex.: Java).

2.3.2 Sobrecarga de Operadores


A sobrecarga, no contexto de linguagens de programação, é a capacidade de fazer com que um
único identificador seja usado denotar várias abstrações.
Usar o mesmo operador para representar funções completamente diferentes prejudica a legibilidade.
Erros de digitação ficam mais difíceis de serem detectados.
Algumas linguagens (notadamente as que permitem tipos abstratos) permitem que o programador
sobrecarregue operadores. Quando bem usado, esse recurso pode aumentar a legibilidade.
Se diversas bibliotecas sobrecarregarem os mesmos operadores, podem surgir conflitos.

2.3.3 Conversões de tipo


Podem ser de estreitamento (inseguras) ou alargamento (o novo tipo inclui aproximações para todos
os valores do tipo original). Os nomes “alargamento” e “estreitamento” não têm relação com o
espaço ocupado em memória pelos tipos envolvidos. A definição dessa classificação não é precisa,
assim, nem sempre é conveniente classificar determinada conversão em uma dessas categorias.
Podem ser explícitas ou implícitas. Às vezes, o termo coerção é usado somente para as conversões
implícitas. O autor deste texto usa a palavra coerção como sinônimo de conversão.
A maioria das linguagens oferece instruções para conversão de tipos, que podem ser apresentadas
como elementos sintáticos da linguagem ou na forma de funções.

2.3.3.1 Coerção em expressões


Se uma linguagem permite expressões aritméticas com operadores que usam operandos de tipos
diferentes (expressões de modo misto), devem definir conversões implícitas.
Em muitas linguagens as conversões implícitas de estreitamento causam a emissão de avisos por

prof. Bruno Schneider


Notas de Aula de GCC105 - BCC e BSI - UFLA

parte do compilador.
Não existe um consenso a respeito do que é melhor: restringir a flexibilidade das expressões ou
deixar a verificação de tipos sob a responsabilidade do programador.
As coerções de subfaixas de inteiros para inteiros, ou de inteiros pequenos para inteiros são muito
comuns.
Coerções mais profundas como a conversão de strings em valores numéricos são mais comuns em
linguagens com vinculação dinâmica de tipos.

2.3.4 Operadores relacionais e expressões booleanas


Linguagens oferecem operadores relacionais para construir expressões booleanas. Operadores
relacionais retornam um valor do tipo booleano (se ele for um tipo da linguagem).
Operadores relacionais costumam estar disponíveis para tipos numéricos, strings e ordinais
(enumerados e subfaixas).
Expressões booleanas são aquelas que retornam um tipo booleano (o operador mais externo é
relacional).
Um efeito interessante da falta do tipo booleano na linguagem C é validade da expressão (a > b > c)
que não tem o mesmo significado que se espera pela notação matemática.

2.3.5 Avaliação em curto-circuito


Muitas vezes, para se determinar o valor de uma expressão, não é preciso avaliar todos os
operadores dela. Quando a avaliação de expressões é feita levando em conta esses casos especiais,
ela é dita uma avaliação em curto-circuito.
A avaliação em curto-circuito é incomum para expressões aritméticas, mas é comum para
expressões booleanas.
Na avaliação em curto-circuito, funções que mudam o estado de algo e retornam valor podem não
ser chamadas, não causando qualquer mudança de estado. Por outro lado, nas avaliações em circuito
longo, operações ilegais podem ser executadas caso o programador esperasse ver apenas uma parte
da expressão processada.
Algumas linguagens tem operadores específicos para os dois tipos de avaliação (ex.: and then/or
eles de Ada). Usar operadores lógicos bit a bit pode prover flexibilidade (ex.: & e | no C, C++ e
Java).

2.3.6 Operador de atribuição

2.3.6.1 Atribuição simples


É a forma tradicional, em que uma variável recebe o valor de uma expressão. O operador pode
retornar algo ou não.

2.3.6.2 Alvos múltiplos


Várias variáveis recebem o valor de uma expressão. Esse efeito pode ser imitado pelos operadores
que retornam referências.

prof. Bruno Schneider


Notas de Aula de GCC105 - BCC e BSI - UFLA

2.3.6.3 Alvos condicionais


Usando o operador ternário ?:, C++ e Java permitem alvos condicionais.

2.3.6.4 Atribuição composta


É a atribuição usando expressões em que a variável modificada faz parte da expressão (ex.: += em
C, C++ e Java).

2.3.6.5 Operador unário de atribuição


Incrementam e decrementam, como nas linguagens C, C++ e Java.

2.3.6.6 Atribuição como função


O operador de atribuição pode retornar valor como em C, C++ e Java.
Facilitam a atribuições seguidas de teste. Permitem imitar os alvos múltiplos. Reduzem a
legibilidade e, na falta do tipo booleano, reduzem a capacidade do compilador para detectar erros.
Java não tem o problema das atribuições no meio de expressões (de C e C++) porque tem o tipo
booleano e o uso de número em lugar de booleanos causa erros de tipo.

2.3.7 Atribuição de modo misto


As linguagens podem permitir atribuição do valor de um tipo à uma variável de outro tipo. Pode-se
usar coerção.

2.4 Controle de fluxo


O controle de fluxo é a determinação sobre a ordem das instruções de um programa. As regras de
precedência e associatividade de operadores, vista na seção anterior, são uma forma de controle de
fluxo. As linguagens também precisam prover formas de controlar o fluxo entre os vários blocos de
programa.
O controle de fluxo é tão fundamental nos programas imperativos quanto a atribuição. Mecanismos
de seleção de instruções permitem produzir repetições ou escolher entre várias formas de computar
um valor.
Duas formas de controle (o desvio condicional/incondicional) são necessárias. Duas formas são
recomendadas: o desvio condicional e a repetição com teste no início. As linguagens proporcionam
mais formas, porém não muitas, para que não fiquem complicadas demais.
As instruções de controle devem ter entrada única e saída única. As entradas múltiplas são perigosas
enquanto que as saídas múltiplas apenas reduzem a legibilidade.

2.4.1 Instruções compostas


São blocos de instruções tratados como se fossem uma única instrução. Eliminam a necessidade de
instruções delimitadas e são especialmente interessantes para as estruturas de controle de fluxo.
Também auxiliam a manter uma “lógica positiva”, que destaca os casos especiais.
Algumas linguagens exigem que instruções compostas apareçam em determinados contextos.

prof. Bruno Schneider


Notas de Aula de GCC105 - BCC e BSI - UFLA

Quase sempre, as instruções compostas são demarcas por algum elemento sintático (ex.: begin/end),
porém algumas linguagens modernas usam a endentação para demarcação.

2.4.2 Instruções de seleção


Permitem escolher caminhos de execução.
Do ponto de vista do hardware, geralmente existem dois tipos de instrução de controle de fluxo:
desvio condicional e incondicional. Esses dois elementos podem ser combinados em instruções de
seleção mais abstratas. As linguagens procuram esconder o desvio incondicional do programador,
oferecendo seletores.

2.4.2.1 Seleção unidirecional e bidirecional


O seletor unidirecional “if (teste) instrução” é uma abstração quase direta do desvio condicional do
hardware.
Para desincentivar o uso de desvio incondicional, as linguagens oferecem instruções compostas ou
algum tipo de marcador para o final das instruções.
O seletor bidirecional “if (teste) then instrução else instrução” é o principal seletor em todas as
linguagens. Geralmente o seletor unidirecional incorpora algum elemento sintático (como o “then”)
de forma a se tornar uma versão especial do seletor bidirecional.
O teste é quase sempre uma expressão booleana.

2.4.2.2 Seletores aninhados


Quando uma instrução de seleção pode ser aninhada em outra, podemos ter ambiguidade. Essa
ambiguidade pode ser resolvida por:
• uma regra da linguagem (ex.: Pascal / o else é sempre vinculado ao if mais recente);
• sintaxe da linguagem (ex.: Algol60 / o if não pode ser usado como instrução de outro if,
exceto se estiver numa instrução composta; Python / uso de elif);
• endentação.
Algumas linguagens usam sequências de instruções (em oposição a instruções compostas) nos
seletores, o que exige um marcador de final e elimina a ambiguidade.

2.4.2.3 Seleção múltipla


Alguns seletores permitem escolher dentre qualquer quantidade de instruções (ex.: case/switch).
Questões de projeto:
• Qual o tipo da expressão de seleção?
• Que tipo de instruções podem ser usadas (sequências, instruções compostas, etc.)?
• Somente um segmento pode ser executado?
• O que fazer se o valor da expressão não foi representado?
Em muitas linguagens, a seleção múltipla requer números inteiros, algumas permitem qualquer tipo

prof. Bruno Schneider


Notas de Aula de GCC105 - BCC e BSI - UFLA

discreto.
Nem sempre a seleção múltipla por valores discretos é adequada. Várias linguagens oferecem a
possibilidade de aninhar testes nos seletores bidirecionais (elseif/elsif/elif), tornando-os seletores
múltiplos que não sofrem da falta de legibilidade do aninhamento profundo de seletores
bidirecionais. Esse tipo de estrutura é mais genérica que a anterior (case/switch).

2.4.3 Instruções iterativas


São instruções que proporcionam repetição da execução de um bloco (laço).

2.4.3.1 Laços controlados por contador


São interessantes para processamento de vetores.
Parametrizadas por “valor inicial”, “valor final” e “passo”.
As linguagens variam muito a respeito dos laços controlados por contador. As questões de projeto
são:
• o tipo da variável de controle;
• o escopo da variável de controle;
• o valor da variável ao final do laço (última atribuição / indefinido);
• a validade de alterar os parâmetros e, caso seja válido, o efeito da alteração;
• a avaliação única dos parâmetros ou a cada iteração.
Algumas formas são muito flexíveis e complicadas (C / Algol60) permitindo inclusive a mistura
entre o controle numérico e lógico.

2.4.3.2 Laços controlados por lógica


A repetição é controlada por uma expressão booleana.
São mais genéricos que os controlados por contador.
Podem realizar o teste antes ou depois do laço.

2.4.3.3 Controle localizado pelo usuário


Para aumentar a flexibilidade dos laços, é comum encontrar mecanismos de controle que podem ser
colocados em qualquer instrução interna ao laço. Essas instruções são comuns para tratamento de
situações especiais, e podem aumentar a velocidade do código ao custo de legibilidade.
Algumas linguagens oferecem estruturas de laço sem controle (loop infinito) ou determinam que o
controle é opcional.
As estruturas de controle envolvem saída prematura do laço (talvez de um laço mais externo) e
volta prematura ao teste de controle (talvez de um laço mais externo).

2.4.3.4 Controle por estruturas de dados


Pode ser interessante fazer uma repetição para cada elemento de um conjunto ou para cada parte de

prof. Bruno Schneider


Notas de Aula de GCC105 - BCC e BSI - UFLA

uma estrutura de dados.


Na sua forma mais simples, um elemento sintático proporciona a repetição para todos os elementos
de uma coleção (ex.: o comando for em Java, a partir da versão 5).
Na forma mais complexa, uma estrutura de dados definida pelo usuário, mais um procedimento de
iteração (que diz como identificar o próximo elemento) podem ser percorridos por um laço que é
repetido a cada elemento da estrutura. A flexibilidade do comando for de C++ e Java são
especialmente interessantes nesse caso.

2.4.4 Desvio incondicional


O desvio incondicional, usualmente chamado de “goto” foi assunto de maior debate quando as
bases da programação estruturada estavam sendo formadas. Eles são perigosos e eram na época o
principal fator a afetar negativamente a legibilidade dos programas. Ainda assim, sua utilidade é
enorme o que motivou tantos debates.
Hoje em dia, a maioria das linguagens de programação ainda oferece essa instrução, porém
juntamente com estruturas de controle que desincentivam ou limitam o seu uso. Linguagens que
apareceram a partir da década de 90 como Java, já começaram a abandonar a instrução.
Essa instrução ainda é parte fundamental do conjunto de instruções de qualquer processador e é
usada pelos compiladores para produzir as estruturas de controle de alto nível das linguagens em
geral. A tendência é que seja eliminada apenas dos códigos de alto nível.
O desvio incondicional funciona em conjunto com rótulos, que são identificadores (às vezes
numéricos) para instruções de um programa.
Para reduzir os problemas relacionados com o desvio incondicional, as linguagens proporcionam
formas limitadas de uso desse comando. Frequentemente as limitações incluem desviar somente
“para frente” e mesmo assim, somente para o comando imediatamente após um bloco. Nessa forma,
o comando não diz para onde o fluxo de instruções será desviado, evitando abusos por parte do
programador. Exemplos comuns são o break para sair de laços e o return para sair de
subprogramas. Algumas linguagens procuram desincentivar até mesmo essa forma limitada de
desvio, como por exemplo, em Pascal e Python, existe uma variável especial para definir o valor
que uma função retorna (em oposição a algo tipo “return”), dificultando a criação de saídas
múltiplas.

2.4.5 Comandos protegidos


Uma outra forma de seleção muito diferente e incomum são os comandos protegidos, que tem a
seguinte forma original:
if <expressão booleana> → <instrução>
[] <expressão booleana> → <instrução>
[] ...
fi

A instrução executada é escolhida de forma não determinística dentre as demarcas por expressões
verdadeiras.
É uma construção interessante para programação concorrente.

prof. Bruno Schneider


Notas de Aula de GCC105 - BCC e BSI - UFLA

2.5 Subprogramas
São essenciais para a abstração de um programa, permitindo que processos completos possam ser
abstraídos numa entidade única e reusados em vários contextos. Ajudam a ressaltar a estrutura geral
de um programa.
É desejável permitir a compilação de subprogramas para criação de bibliotecas (também chamadas
de “pacotes”).

2.5.1 Fundamentos dos subprogramas

2.5.1.1 Características gerais


Subprogramas (exceto as co-rotinas), tem as seguintes características:
• um único ponto de entrada;
• o início da execução suspende a execução da unidade chamadora;
• o término da execução faz a execução da unidade chamadora continuar.

2.5.1.2 Definições básicas


Uma “definição de subprograma” descreve a interface (forma para trocar informações com o
chamador) e o processo (ações). Também chamado de “implementação”.
Uma “chamada a subprograma” é a solicitação para executar o processo.
Um subprograma está “ativo” se a sua execução começou mas ainda não terminou.
Uma “declaração de subprograma” (também chamada de “cabeçalho”, “declaração avançada” ou
“protótipo”) identifica um bloco como sendo um subprograma (geralmente por meio de uma
palavra especial) de um determinado tipo (ex.: procedimento/função), dá um nome para ele e (não
obrigatoriamente) identifica seus parâmetros. Pode vir separada da definição, para fins de
verificação estática de tipos.
Os parâmetros, quando vistos como instâncias de valores ou no ponto de vista da unidade
chamadora, são chamados “argumentos”. Também podem ser chamados, respectivamente, de
“parâmetros formais” e “parâmetros reais”.
O “perfil de parâmetro” é o número, a ordem e os tipos dos parâmetros.
O “protocolo” de um subprograma é o “perfil de parâmetros” mais o tipo de retorno se o
subprograma for uma função. Pode ser visto como o tipo do subprograma.

2.5.1.3 Parâmetros
São uma forma de fornecer os dados para o processamento de um subprograma. Deixam o
subprograma mais legível que o uso de variáveis externas. O escopo reduzido ajuda a evitar erros.
Parâmetros podem ser dados ou subprogramas.
A vinculação entre parâmetros reais e formais por ser feita pela posição ou pelo nome. Em algumas
linguagens pode-se usar a duas formas ao mesmo tempo (ex.: Ada e Python).
Nem sempre é obrigatório usar todos os parâmetros. Nesse caso é preciso existir alguma regra a

prof. Bruno Schneider


Notas de Aula de GCC105 - BCC e BSI - UFLA

respeito daqueles não vinculados. Geralmente, declara-se um valor padrão.


Nem sempre é preciso definir um número fixo de parâmetros. Nesses casos, parâmetros são
geralmente vistos como elementos de um vetor ou lista.
São geralmente variáveis stack-dinâmicas.

2.5.1.4 Procedimentos e Funções


Procedimentos produzem resultados alterando estados de variáveis no programa. Às vezes seus
parâmetros são apelidos (aliases) para variáveis externas, permitindo assim um escopo reduzido e
boa legibilidade para os elementos importantes do subprograma.
Funções são modelos das funções matemáticas mas podem apresentar efeitos colaterais. São
“substituídas” por um valor e geralmente usadas dentro de expressões. Podem ser entendidas como
operadores.
Em algumas linguagens (como C, C++ e Java) não existem procedimentos e as funções não
precisam fazer parte de uma expressão.

2.5.2 Ambientes de referência locais


Subprogramas são peças importantes do controle de escopo, indicando limites para a vinculação de
nomes.
Geralmente as variáveis locais são stack-dinâmicas. Para permitir maior velocidade, algumas
linguagens ainda usam a vinculação estática ao armazenamento, usando variáveis stack-dinâmicas
somente nos subprogramas recursivos. Em algumas linguagens é necessário declarar explicitamente
quando um programa é recursivo.

2.5.3 Métodos de passagem de parâmetros


A vinculação entre os parâmetros reais e os formais pode ser para (a) transmitir dados, (b) receber
dados ou (c) ambos. Esses três modelos são conhecidos por (a) modo entrada, (b) modo saída ou (c)
modo entrada/saída.

2.5.3.1 Passagem por valor


O valor do parâmetro real é usado na inicialização do parâmetro formal. Esse, por sua vez, funciona
como uma variável local. Implementa o “modo entrada”.
É um método seguro, mas requer tempo e armazenamento extra para a cópia.
A passagem de ponteiros por valor pode ser confundida com a passagem por referência quando o
valor de uma variável é uma referência (ex.: ponteiros). Nesse caso, a passagem por valor pode
causar o mesmo efeito de uma passagem por referência (alteração do valor do parâmetro real). Na
linguagem Java, em que as variáveis podem ser referências, sem que isso fique explícito para o
programador, essa confusão é maior.
A passagem por valor deve ser preferida pelo programador nos casos em que se deseja o modo de
entrada e que a quantidade de informações passadas é pequena.

prof. Bruno Schneider


Notas de Aula de GCC105 - BCC e BSI - UFLA

2.5.3.2 Passagem por resultado


O parâmetro funciona como uma variável local, cujo valor é copiado para o parâmetro real ao final
do subprograma. Implementa o “modo saída”.
Pode causar problemas na ocorrência de colisão entre parâmetros reais (um único parâmetro real
vinculado a dois ou mais parâmetros formais). Pode causar semântica ambígua em função do
momento em que se determina o endereço de retorno.
Assim como a passagem por valor, exige mais tempo e armazenamento.
Os compiladores costumam usar este método para retornar o valor de função. Esse retorno é um
parâmetro especial, no sentido de que ele não aparece junto dos outros parâmetros e o parâmetro
real não é vinculado a um nome. Isso ajuda na redução dos problemas do método.

2.5.3.3 Passagem por valor-resultado


É a cópia na chamada e no encerramento do subprograma. Implementa o “modo entrada/saída”.
Tem as desvantagens da passagem por valor e as da passagem por resultado.

2.5.3.4 Passagem por referência


Transfere uma referência (geralmente um endereço) ao invés do valor, de forma que o parâmetro
formal passa a ser um apelido (alias) do parâmetro real. Implementa o “modo entrada / saída”.
Essa forma de passagem de parâmetros é tida como rápia porque uma referência é uma quantidade
pequena de informação e tem sempre o mesmo tamanho, independentemente do tamanho do valor a
que se refere. Entretanto, é possível que o tempo e espaço necessários para passagem de uma
referência sejam iguais ou maiores que os necessários para a cópia de um valor. Assim, a passagem
por referência de grandes quantidades de informação é mais rápida que a passagem por valor, o que
torna este método vantajoso mesmo em situações nas quais o programador deseja o modo de
entrada.
A passagem por referência cria efeitos colaterais e por isso deve ser evitada quando se deseja apenas
o “modo entrada” de pouca informação.
Da mesma forma que a passagem por resultado, pode causar problemas na ocorrência de colisão de
parâmetros reais. Pode criar apelidos (aliases) que dificultam a leitura do código (ex.: o parâmetro
pode ser um alias para uma variável global).
Apesar de ser bastante comum, costuma não ser oferecida por linguagens que escondem o
gerenciamento de memória do programador (ex.: Java).
Algumas linguagens permitem a passagem por referência constante, na qual o parâmetro formal é
tratado como constante, mesclando as vantagens deste método com a segurança do modo de entrada
(ex.: C++). Nas linguagem sem esse recurso, a passagem de uma constante por referência não deve
ser permitida pelo compilador, pois poderia permitir a alteração do valor de uma constante.

2.5.3.5 Passagem por nome


A passagem por nome é muito diferente das outras formas. Nela, o parâmetro formal é textualmente
substituído pelo parâmetro real. Isso pode ser implementado de várias formas, com semânticas
diferentes.

prof. Bruno Schneider


Notas de Aula de GCC105 - BCC e BSI - UFLA

Por exemplo, se o parâmetro real for uma expressão constante, pode-se usar uma implementação
como a da passagem por valor (semântica de entrada). Se for uma variável, pode-se usar uma
implementação como a passagem por referência (semântica de entrada/saída).

2.5.4 Parâmetros que são nomes de subprogramas


Muitas vezes, é desejável passar um subprograma como argumento para outro. Isso permite um
maior reaproveitamento de código e ajuda a manter os vários subprogramas pequenos. Por exemplo,
um subprograma de ordenação pode receber entre seus parâmetros uma função de comparação.
Assim a ordenação será feita conforme o critério desejado (ex.: nomes de pessoas podem ser
ordenados pelo primeiro nome ou pelo sobrenome).
Porém é desejável que o compilador possa verificar a quantidade de parâmetros e seus tipos na
chamada ao subprograma. Assim além da informação a respeito de qual subprograma deve ser
usado, é desejável se ter a informação a respeito de como ele deve ser chamado, ou seja, o protocolo
do subprograma deve ser visto como seu tipo.
Mais ainda, é desejável que essa informação esteja disponível na declaração do subprograma, para
permitir que ele seja compilado de forma independente do subprograma que é seu parâmetro.
Uma questão importante é o ambiente de referenciamento: variáveis externas ao subprograma que é
usado como parâmetro são buscadas em que ambiente? As linguagens definem respostas diferentes,
listadas a seguir em ordem de popularidade:
• vinculação profunda: o ambiente de referenciamento é aquele em que o subprograma foi
declarado (a posição no texto do código define a vinculação);
• vinculação rasa: o ambiente de referenciamento é aquele em que o subprograma foi ativado
(a ordem execução define a vinculação) - esse tipo é considerado mais perigoso e vem sendo
abandonado;
• vinculação ad hoc: o ambiente de referenciamento é aquele em que o subprograma foi usado
como parâmetro.

2.5.5 Subprogramas sobrecarregados


Assim como os operadores, subprogramas podem ser sobrecarregados. Para tal é preciso que cada
versão do subprograma tenha um protocolo único. A maioria das linguagens, entretanto, exigem que
eles tenham um perfil de parâmetro único. Essa escolha está diretamente relacionada com a forma
pela qual a linguagens trata expressões de modo misto. No primeiro caso, diz-se que a linguagem
oferece “sobrecarga dependente de contexto” e no segundo caso “sobrecarga independente de
contexto”.
A possibilidade de omitir parâmetros interfere com a possibilidade de sobrecarga, podendo criar
ambiguidades. Entretanto essas ambiguidades podem ser detectadas pelo compilador.
A sobrecarga é considerada um tipo de polimorfismo (polimorfismo ad hoc). Seu uso pode levar à
implementação de subprogramas exatamente iguais em termos de instruções, mas que diferem em
termos de tipos.

2.5.6 Subprogramas genéricos


São aqueles que não especificam completamente todos os tipos. A possibilidade de não vincular

prof. Bruno Schneider


Notas de Aula de GCC105 - BCC e BSI - UFLA

parâmetros a um tipo de dados numa linguagem é uma forma poderosa de polimorfismo, conhecida
como “polimorfismo paramétrico”. A viabilização disso numa linguagem com tipificação forte
passa pela especificação de tipos genéricos.
Subprogramas genéricos são aqueles que apresentam um ou mais parâmetros de tipo genérico.
Exemplos de linguagens que implementam tipos genéricos: Ada, C++, Haskell e Java (a partir da
versão 1.5).
Ada foi a primeira linguagem com uma implementação para subprogramas genéricos. Nela (e em
C++, etc.) A implementação de um subprograma genérico não é transformada em código pelo
compilador e não tem efeito sobre o programa. Ela serve como um modelo (gabarito) para o
compilador que usa esse modelo para gerar código para todos os tipos que são usados na chamada
do subprograma.
De maneira mais imprecisa, pode-se dizer que o compilador gera código para vários subprogramas
com o mesmo nome, um para cada tipo usado nas chamadas, tirando do programador o trabalho de
sobrecarregar o subprograma. Em Ada, é preciso instanciar os subprogramas de forma explícita. Em
Java, o subprograma genérico gera byte-code como código comum1.

2.5.7 Compilação Separada e Independente


A compilação de partes de programas é importante. Existem duas abordagens para permitir isso:
“compilação separada” e “compilação independente”. Essas partes de programas podem ser
chamadas de “unidades de compilação”.
Na compilação separada, unidades de compilação precisam de informações definidas em outras
unidades de compilação. Essas informações são usadas para verificação de tipos, então em geral
compreendem os protocolos de subprogramas.
Na compilação independente, a compilação de uma unidade não usa informações de outras,
tornando inviável a verificação de tipos em tempo de compilação.

2.5.8 Questões de projeto referentes a funções


As várias linguagens de programação diferem no que diz respeito a permitir efeitos colaterais em
funções (por causa das várias complicações decorrentes - mencionadas anteriormente) e com
relação aos tipos válidos de retorno.
A maioria das linguagens permite os efeitos colaterais e limita os tipos de retorno aos valores
escalares (Ada é uma das exceções nos dois aspectos).
As restrições aos tipos de retorno são parcialmente justificadas pelo fato de que a troca de valores é
geralmente feita via passagem por resultado, causando ineficiência para grandes quantidades de
informação. Uma exceção comum é o tipo registro (ou tuplas) que de certo modo podem ser usados
para retornar mais de um valor.
Nas linguagens que permitem retornar tipos compostos, a atribuição entre esses tipos pode ser
interessante para separação dos valores em variáveis independentes. Ex.:
(encontrado, indice) := Buscar(vetor, elemento);
if (encontrado)
then print(“Encontrado na posição”,indice);
1 Generics Tutorial - Generics in the Java Programming Language (http://java.sun.com/j2se/1.5/pdf/generics-
tutorial.pdf) - pág.3.

prof. Bruno Schneider


Notas de Aula de GCC105 - BCC e BSI - UFLA

else print(“Não encontrado”);

2.5.9 Co-rotinas
São um tipo bem diferente de subprograma. Elas se prestam à uma relação de igual para igual entre
unidade chamadora e chamada. Ainda assim, co-rotinas são hierarquicamente ligadas à uma unidade
de programa chamada “unidade mestra”.
A unidade mestra cria uma série de co-rotinas que “se conhecem” e as inicializa. Em seguida, o
controle é passado para uma delas que passam o controle adiante para outra co-rotina do grupo e
assim sucessivamente até eventualmente todas acabam e o controle retorna à unidade mestra.
As co-rotinas não tem um único ponto de entrada e mantém uma memória de suas ativações
anteriores. Os pontos em que passam o controle adiante são determinados pelo programador e cada
vez que uma co-rotina ganha o controle de fluxo, ela retoma a execução de onde parou. A invocação
de uma co-rotina é chamada de “retomada”.
As co-rotinas apareceram na linguagem SIMULA que tinha o propósito de oferecer facilidade para
simulação de sistemas. Um dos elementos desejados na linguagem era uma forma de proporcionar a
execução em tempo compartilhado entre unidades de programa, como se elas estivessem
executando ao mesmo tempo.
São geralmente implementadas na forma de laços que passam adiante o controle de fluxo a cada
repetição.
As co-rotinas formaram a base para a programação concorrente e são oferecidas em linguagens de
programação que enfocam esse tipo de programação. Elas também são a base conceitual do
conceito de multitarefa cooperativa.
São tidas como mais genéricas que os subprogramas comuns.

2.6 Tipos Abstratos de Dados


A abstração é importante não só nos processos como também nos dados.
Um tipo abstrato de dados (TAD) é um encapsulamento que inclui a representação de dados de um
tipo mais os subprogramas que fornecem as operações para este tipo. Uma instância de um TAD é
um objeto.
Um encapsulamento é um agrupamento de subprogramas e os dados que eles manipulam. Assim,
um TAD é caso especial de encapsulamento. Suporte aos TADs como única forma de
encapsulamento pode trazer problemas relativos a operações entre tipos diferentes (ex.: somar ponto
com vetor).
Podemos entender o ponto flutuante como uma abstração de dados. Variáveis do tipo ponto
flutuante são usadas para representar um tipo de informação (número racional). Elas podem ser
manipuladas com um conjunto de operações (somar, multiplicar, etc.). O programador não precisa
conhecer a sua representação interna para usá-lo, não pode alterar diretamente as partes
fundamentais da sua representação e não pode construir novas operações para ele, a não ser pela
composição das operações existentes.
As linguagens de programação passaram a permitir que o programador crie TADs,
preferencialmente de forma que as definições do tipo e de suas operações ficam numa unidade de
compilação, enquanto que outras unidades de compilação podem declarar variáveis desse tipo e
usar a operações definidas para ele.

prof. Bruno Schneider


Notas de Aula de GCC105 - BCC e BSI - UFLA

Esconder os detalhes de um tipo de dados é importante para manter a independência entre módulos
de um programa. Por exemplo: suponha que um compilador de uma determinada linguagem usa
mais bits para a mantissa de um número de ponto flutuante do que outro compilador. Nesse caso, os
programas desenvolvidos funcionam nos dois casos, e a mudança na representação de um deles não
necessita mudança no código que faz uso dele.

2.6.1 Questões de projeto


Os TADs são mais bem suportados quando a linguagem permite deixar seus nomes e os protocolos
de suas operações acessíveis à qualquer unidade de compilação, sem tornar a estrutura interna do
tipo e a implementação de suas operações acessíveis também.
Algumas operações genéricas podem ser pré-determinadas pela linguagem. Ex.: verificação de
equivalência, atribuição.
Algumas operações genéricas podem ser obrigatoriamente declaradas pelo usuário. Ex.:
comportamento de iteradores, inicialização.
As linguagens diferem ainda a respeito de restrições para os TADs (ex.: todo TAD deve ser um tipo
de ponteiro), se eles podem ser parametrizados (ex.: vetor de ...) e o controle de acesso (permitir
restrições ao uso desses tipos).
Algumas linguagens oferecem formas de encapsular vários tipos e suas operações. Esse recurso é
mais geral que a definição de um TAD e pode ser usado com esse propósito.

2.6.2 Suporte nas linguagens


Por ser a base da orientação a objetos (OO), os TADs são suportados por qualquer linguagem que
suporte o conceito da OO.
A primeira linguagem a dar um passo nessa direção foi SIMULA 67, que permitia a declaração de
“classes”. As classes eram tipos de dados declarados de tal forma que os dados e subprogramas
ficavam encapsulados. Não havia nenhum controle de acesso e os dados poderiam ser acessados
diretamente pelos clientes da classe. Isso gera problemas de confiabilidade e também faz com que
esse encapsulamento nem sempre seja considerado um suporte aos TADs.
A linguagem Ada oferece encapsulamento na construção de unidades chamadas “pacotes”. Pacotes
não são tipos, mas sim coleções de tipos. Ada permite a separação entre declarações e
implementações. Oferece também suporte à ocultação de informações, proporcionando uma forma
de evitar que os clientes tenham acesso às partes internas dos TADs. Ada oferece ainda, operações
padrão de atribuição e comparação de equivalência e desigualdade, à critério do programador, que
pode suprimi-las ou redefini-las.
A fim de restringir o acesso às partes de um TAD para o cliente e ao mesmo tempo oferecer essa
informação ao compilador, Ada usou uma palavra especial (private) para modificar declarações,
informando que elas ficam inacessíveis ao cliente.
A linguagem Modula-2 oferece “módulos” que são muito semelhantes aos “pacotes” de Ada, com a
restrição de que os tipos definidos em módulos são sempre ponteiros. Dessa forma, o conhecimento
da estrutura interna dos módulos não é necessária nem mesmo ao compilador e as declarações
separadas das implementações não são necessárias.
Em C++ a estrutura que proporciona encapsulamento é a classe. Classes são tipos de dados. A
ocultação de informações funciona de maneira semelhante à da Ada. Essas duas características

prof. Bruno Schneider


Notas de Aula de GCC105 - BCC e BSI - UFLA

juntas proporcionam um suporte mais direto aos TADs. Outras facilidades oferecidas são os
construtores e destrutores que podem ser usados para gerenciar a inicialização, alocação e
desalocação de recursos (ex.: memória no heap).
O suporte de Java é muito parecido como o de C++, com algumas diferenças importantes. Todos os
tipos definidos pelo programador em Java devem ser classes. Todas as variáveis que são TADs são
alocadas no heap e acessadas por meio de referências. Além das classes, Java permite a definição de
pacotes (encapsulamento geral) e oferece o “escopo de pacote”, em que partes protegidas das
classes são acessíveis em outras partes do pacote.

2.6.3 Tipos abstratos de dados parametrizados


Assim, como os subprogramas, tipos de dados parametrizados são de grande utilidade. Eles
permitem generalizar tipos de dados, descrevendo tipos como “lista de qualquer coisa”.
Ada, C++ e Java são exemplos de linguagens que proporcionam esse tipo de construção. Assim
como nos subprogramas, elas usam o código como modelo para a construção de vários pacotes ou
classes conforme os casos são instanciados.

2.7 Tratamento de erros


A execução de programa está sujeita a vários tipos de erros. Alguns erros acontecem ao nível de
hardware (ex.: overflow aritmético) e outros acontecem ao nível de software (ex.: tentar remover
elemento que não existe numa coleção). Esse último tipo pode ser tratado pelo compilador (quando
possível) ou pela própria execução do programa.
As linguagens de programação devem facilitar o tratamento dessas condições de erro, facilitando a
tarefa de produzir programas que lidam graciosamente com elas.
Algumas linguagens determinam que seja gerado código para detecção de erros em várias situações
(ex.: valor inválido para o índice de um vetor), mas é desejável que permitam ao programador
especificar o que deve ser feito nas situações de erro.

2.7.1 Código de Erros


A forma mais primitiva para lidar com erros é usar seletores que fazem uso das formas disponíveis
para detecção de erros. Assim, as instruções de um programa são frequentemente seguidas de
instruções que desviam o fluxo de instruções caso uma situação anormal seja detectada.
O uso de funções que relatam algum tipo de status da execução é comum na hora de projetar
subprogramas.
Programas também retornam esse status de tal forma que os programas que os chamaram também
possar verificar problemas na execução para tomar as medidas necessárias. Dessa forma, linguagens
como C++ exigem que o bloco principal de um programa retorne um número inteiro. Por
convenção, esse número deve ser zero para indicar um execução sem erros, enquanto que qualquer
outro número indica algum tipo de erro de execução. Cada programa cria o significado dos valores
que indicam erros, muitas vezes, tratando o valor como um vetor de bits em que cada bit indica a
ocorrência de um tipo de erro.
O uso de funções que retornam status tem as desvantagens de (a) criar um código em que a
condições anormais ficam misturadas com a condição normal e (b) dificultar o projeto de funções
que retornam o resultado de um processamento (além do status). A segunda pode ser reduzida pela

prof. Bruno Schneider


Notas de Aula de GCC105 - BCC e BSI - UFLA

tipificação dinâmica, que permite a criação de uma função que retorna algum tipo que representa
erro ou um tipo que representa o resultado do processamento.
Nas bibliotecas padrão da linguagem C, o valor de retorno de um subprograma é geralmente usado
como código de erro.

2.7.2 Manipulação de Exceções


Algumas linguagens oferecem construções para tratar condições anormais de execução detectadas
por hardware e software, inclusive condições incomuns (não errôneas) em geral de uma mesma
forma. O uso dessas construções é chamado de manipulação de exceções.
Essas construções tem a desvantagem de adicionar uma boa dose de complexidade à linguagem e
consequentemente ao seu compilador. A vantagem da manipulação de exceções é o aumento da
legibilidade e a facilidade para descrever as formas de tratar erros, o que funciona como um
incentivo ao tratamento de erros.

2.7.2.1 Questões de projeto


A manipulação de exceções deveria proporcionar formas de reusar código para tratamento de
exceções (ex.: se qualquer uma dessas divisões der erro faça aquilo) ao mesmo que fornece formas
de ligar tratadores a casos especiais de execução (ex.: essa divisão por zero é especial e deve ser
tratada de forma diferente).
É desejável permitir que o tratamento de exceções seja ligado e desligado tanto em tempo de
execução quanto em tempo de compilação.
Outras questões de projeto da linguagem incluem:
• como e onde especificar os tratadores (subprogramas? trechos de código?), além de como
determinar seu escopo;
• como vincular a ocorrência de uma exceção à execução do tratador;
• como e onde retomar a execução do programa depois do tratamento;
• como o usuário pode especificar exceções;
• a existência de tratadores padrão;
• a criação automática de exceções para condições comuns de erro;
• a geração de exceções predefinidas pelo próprio programa;
• a possibilidade de desativar e reativar exceções.
...

2.8 Sistemas de tipo


A checagem de tipos é, em geral, benéfica. As linguagens precisam de sistemas para garantir a
checagem, preferencialmente, em tempo de compilação. Portanto, um sistema de tipos (no contexto
de linguagens de programação) é o conjunto de regras que permite a classificação de expressões de
acordo com um tipo.
Declarar o tipo de cada variável antes do seu uso é uma forma simples de proporcionar isso, mas

prof. Bruno Schneider


Notas de Aula de GCC105 - BCC e BSI - UFLA

oferece pouca flexibilidade para o programador, limitando o reuso e a facilidade de manutenção.


Para conciliar essas duas necessidades contraditórias, pode-se usar declarações mais gerais, em que
não se define um tipo por completo, mas que fornecem elementos suficientes para a checagem.
O conceito de polimorfismo (tratamento de tipos diferentes de maneira uniforme) é a flexibilidade
desejada e é difícil de conciliar com a verificação de tipos.
Nas linguagens em que os tipos precisam ser completamente determinados, temos monomorfismo.
Naquelas em que o tipo precisa ser parcialmente determinado (somente o suficiente para checagem
de tipos nas expressões) ou em que não há sistemas de tipos, temos polimorfismo.
O polimorfismo pode então ser obtido em níveis variados, ou seja, as linguagens proporcionam
mais ou menos recursos para o tratamento homogêneo de tipos diferentes. Alguns dos recursos que
proporcionam polimorfismo e já foram analisados são:
• vinculação dinâmica de tipos;
• escopo dinâmico;
• sobrecarga de subprogramas (e operadores);
• subprogramas genéricos.
Nenhuma linguagem é completamente monomórfica. Mesmo linguagens como Pascal possuem
subprogramas polimórficos como o write, apesar de não proporcionarem esse tipo de recurso para
o programador. Isso é uma inconsistência da linguagem e os projetos de linguagens mais modernas
procuram oferecer ao programador um sistema de tipos consistente; com um nível maior de
polimorfismo.
O polimorfismo paramétrico dos subprogramas genéricos pode ser usado também na declaração de
tipos. Essa estratégia é extremamente útil em tipos agregados como vetores, listas, árvores, etc.
Nesse tipos, podemos variar o tipo dos valores individuais sem alterar a estrutura geral.
As linguagens funcionais ML e Haskell combinam o polimorfismo paramétrico com a inferência de
tipos, proporcionando a robustez da tipificação forte com a conveniência de escrita da tipificação
fraca. Entretanto, não é recomendável abusar da inferência de tipos. Mais do que declarar tipos para
o compilador, a declaração de tipos serve como uma forma de documentação do código,
evidenciando o significado das coisas.
Alguns sistemas de tipos usam o conceito de herança para permitir restrições aos parâmetros de
tipos. Se considerarmos que alguns tipos são casos especiais de outros, podemos agrupar tipos em
conjuntos. Esses conjuntos podem ser usados como restrições aos parâmetros do polimorfismo
paramétrico.
Por exemplo: números inteiros sem sinal e subfaixas podem ser considerados casos especiais
(subtipos) do tipo inteiro. Algumas linguagens permitem especificar que um subprograma recebe
um valor de um tipo t qualquer, desde que t pertença ao conjunto dos tipos que são números
inteiros. Dessa forma, parâmetros reais de tipos subfaixa são válidos para esse subprogramas mas os
de tipo ponto flutuante não são.
Algumas linguagens como Haskell definem subtipos em função das operações disponíveis para eles
(ex.: o conjunto dos tipos em que existe a comparação de equivalência; conjuntos dos tipos para os
quais existem operações aritméticas, etc.). O programador pode então criar um TAD com as
operações relevantes e incluí-lo em um ou mais conjuntos de tipos.
Outras linguagens como Ada, fornecem formas de declarar subtipos em função de particularidades

prof. Bruno Schneider


Notas de Aula de GCC105 - BCC e BSI - UFLA

das suas instâncias. Por exemplo, suponha o tipo pessoa, como um registro que possui, entre
outras coisas, o campo gênero. É possível então declarar o tipo homem que um caso especial de
pessoa, onde o gênero é masculino.
O termo herança é usando dentro do conceito da orientação, em que, além de vermos tipos como
casos especiais de outros tipos e termos operações comuns a vários tipos, existe também a
preocupação a respeito de como essas operações alteram os valores de um tipo. A ideia básica,
entretanto é a mesma.

3. Eventos
X

Referências
[Budd 91] Budd, Timothy. An Introduction to Object-Oriented Programming. Addison-Wesly.
1991.

prof. Bruno Schneider

Você também pode gostar