Você está na página 1de 8

Objetos de Dados

Na aula passada, vimos que cada variável possui uma área de armazenamento amarrada
a si durante a execução do programa, e o seu o tempo de vida é o período de tempo no qual a
amarração existe. Esta área de armazenamento é
usada para guardar o valor codificado da variável. O termo objeto de dado é usado para
referencia à área de armazenamento em conjunto com o valor da variável. Então o objeto de dado se
refere ao valor da variável e a área de armazenamento utilizada.
Desta forma, um objeto de dado é definido por (L, N, V, T), onde L é a localização (
endereço de memória ), N o nome da variável, V o valor da variável e T o tipo de objeto (
inteiro, decimal, caracteres, lógico, etc ). Todos estes componentes devem ser amarrados.
A figura abaixo mostra a visualização de um objeto de dado e suas amarrações, que são representadas por
linhas que vão do objeto de dado aos objetos correspondentes aos quais são amarrados.

Neste caso, o espaço de armazenamento é “invisível” para o programador, que só tem


conhecimento do nome da variavel, e consiste no conjunto de localizações virtuais de
armazenamento disponíveis na memória do computador.

A vantagem de se usar uma linguagem fortemente tipada é que qualquer erro de


incompatibilidade de tipo consegue ser verificado em tempo de compilação e pode
portanto ser corrigido antes do programa ser executado. Linguagens fracamente tipadas
dão maior liberdade porém não fazem verificação estática, em tempo de compilação, e
portanto podem ocorrer erros de conversão de tipos em tempo de execução, gerando
problemas inesperados.

Na prática erros de conversão de tipo devem ser tratados adequadamente, veremos isso
na próxima aula.s nomes possíveis que podem ser dados aos objetos de dados. E finalmente, o espaço
de tipos é o conjunto de todos os tipos de dados possíveis que podem ser amarrados a um objeto de dado.

Os Tipos de Dados

Programas de computador podem ser vistos como funções que devem ser aplicadas a certos dados
de entrada com objetivo de produzir determinados resultados. Essas funções, por sua vez, possuem uma
seqüência de instruções que produzem dados intermediários que são armazenados em variáveis de
programa. Existem diferenças básicas de uma LP para outra quanto às espécies de dados que usam, às
espécies de operações disponíveis sobre os dados e à maneira como os dados podem ser estruturados e
usados.
As LP geralmente fornecem um conjunto fixo de tipos de dados elementares (ou embutidos), e
mecanismos para definição de tipos de dados mais complexos a partir dos elementares, tais como tipos de
dados estruturados. Todas as LP possuem tipos elementares, cujos valores são atômicos, e tipos
agregados, que consistem numa composição dos tipos elementares. Algumas linguagens também possuem
tipos recursivos, cujos valores podem ser compostos de outros valores do mesmo tipo.

Os tipos de dados elementares são aqueles que contêm somente um único valor de dado e sobre o
qual são definidas algumas operações. Também são conhecidos como tipos de dados escalares. Este tipo
de dado é sempre manipulado como uma unidade e não pode ser decomposto em valores mais simples.
Assim, os inteiros podem ser encarados como o conjunto de valores ..., -2, -1, 0, 1, 2, ... que podem ser
manipulados pelos operadores familiares +, -, * e /. Na verdade, inteiros e reais são suportados por
hardware na maioria dos computadores convencionais, que também fornecem aritmética de ponto fixo e
ponto flutuante.

Existem tipos de dados elementares similares em diferentes LP com diferentes nomes. Por exemplo:
Pascal tem os tipos Boolean, Integer e Real; ML tem os tipos bool, int e real; e C++ tem os tipos bool, int
e float. Os nomes não têm muito significado, o que é importante notar é que cada um dos tipos mais
comuns, tais como boolean, inteiro, real e caracter, são definidos na implementação; isto significa que
eles possuem um conjunto de valores que podem ser definidos diferentemente por cada implementação da
LP. Por exemplo, inteiros consistem
num intervalo de todos os números possíveis e reais são um subconjunto dos números reais. Também
existem limitações de hardware, uma vez que os computadores fornecem tamanhos diferentes para os
tipos aritméticos suportados. Na linguagem C isto pode ser observado pelos prefixos long e short nas
declarações de int e float.

Importante:

Cabe ao programador saber identificar o tipo de dado mais adequado à informação que será lida,
processada ou gerada pelo programa. O uso incorreto de um tipo de dado pode acarretar em perda
de informação ou em erros de lógica difíceis de detectar e que geralmente são detectados apenas
quando o programa já está em uso há um bom tempo em produção. Por isso, é muito importante ao
programador conhecer os tipos de dado de cada linguagem de programação e os limites de valores,
de tamanho, de precisão de casas decimais para cada tipo de dado. No caso específico de tipos
numéricos, é muito importante estar atento aos limites e intervalo de valores que cada tipo de dado
suporta na linguagem de programação que está sendo utilizada.

Tipos Elementares Enumerados

Em Pascal, Ada, C e Java é possível um novo tipo elementar através da enumeração dos seus valores,
mais precisamente, através da enumeração de identificadores que irão denotar seus valores. Tal tipo é
chamado de enumeração. Considere a definição do seguinte tipo em C e Pascal, respectivamente:

Em C um tipo enumerado pode ser declarado assim: enum Month {jan, feb, mar, apr, may, jun, jul, aug,
sep, oct, nov, dec};

Em Pascal um tipo enumerado pode ser declarado assim: type Month = (jan, feb, mar, apr, may, jun, jul,
aug, sep, oct, nov, dec)

Em Java um tipo enumerado pode ser declarado assim: public enum SituacaoIntimacaoEnum {
ABERTA(1, "Aberta"),
REEMITIDA(2, "Reemitida"),
CANCELADA(3, "Cancelada"),
CONCLUIDA(4, "Concluida");

private Integer codigo;


private String descricao;
private SituacaoIntimacaoEnum(Integer codigo, String descricao) {
this.codigo = codigo;
this.descricao = descricao;
}
Este tipo de dado é utilizado para representar um numero finito de elementos de um mesmo tipo de dado e
está presente na maioria das linguagens de programação. O componente de um vetor pode de um tipo de
dado elementar com um tipo de dado agregado.

public static SituacaoIntimacaoEnum valueOf(Integer codigo) {


for (SituacaoIntimacaoEnum a : values()) {
if (codigo.equals(a.codigo)) {
return a;
}
}
throw new IllegalArgumentException("Nao existe Situacao de Intimacao com
codigo " + codigo);
}}
}

Importante: O uso de tipos enumerados é sempre recomendado pois eles permitem


aumentar a clareza e legibilidade de um programa. Usamos tipos enumerados em
situações em que temos um conjunto de valores possíveis para uma variável. Por
exemplo, estado civil ( casado, solteiro, divorciado, viúvo ), grau de instrução (
fundamental, secundário, superior ), sexo ( masculino e feminino ) e situação do
aluno ( aprovado, reprovado, recuperação ) são alguns exemplos de informações
que possuem um conjunto de valores que pode ser representado por uma
enumeração. Veja um exemplo abaixo de uso de enumeração:

if (intimacao.getCsSituacao().equals(SituacaoIntimacaoEnum.CANCELADA.getCodigo()) ) {
excecaoNegocio.addMessage(
"intimacao.validacao.intimacaoCancelada",
"intimacao.validacao.intimacaoCancelada");
}

Com certeza o trecho de código acima é mais legível do que se fosse escrito sem o
uso de enumeração conforme abaixo:

if ( intimacao.getCsSituacao().equals(3) ) {
excecaoNegocio.addMessage(
"intimacao.validacao.intimacaoCancelada",
"intimacao.validacao.intimacaoCancelada");
}

Os tipos de dados agregados são aqueles cujos valores são compostos ou estruturados
a partir dos tipos elementares. Os objetos de dados que fazem parte de um tipo agregado são
chamados de componentes. Cada componente pode ser um tipo de dado elementar ou agregado. Os tipos
de dados agregados possuem um único nome, mas a manipulação pode ser feita no objeto como um todo
ou em um componente de cada vez através de uma operação adequada de seleção.

Na linguagem Pascal temos o tipo Record que representa um agregado de campos que podem ser de tipo
elementar ou de outro tipo record. Como podemos ver nos exemplos abaixo:
type POLIGONO: record
num_lados: integer;
comprimento_lado: real;
end;

type DATA: record


ano: integer;
mes: interger;
dia: integer;
end;

type PEDIDO: record


numero : integer;
dataPedido : DATA;
valor : real;
cpf : integer;
end;

Na linguagem C temos o tipo Struts que representa uma estrutura heterogênea de dados
de diferentes tipos, podendo ser tipo elementar ou outro tipo Struts. Vamos ver o
exemplo abaixo:

struct POLIGONO {
int num_lados;
float comprimento_lado;
}

TIpos de dado com Mapeamento Finito

Uma representação convencional de mapeamento finito consiste em alocar um certo


número de elementos (por exemplo, palavras) para cada componente. As linguagens de programação
implementam o
mapeamento finito através do uso de vetores. As declarações em C e Pascal float
a[10]; var a: array [1 .. 10] of real podem ser vistas como um mapeamento do intervalo de inteiros entre 0
e 9, e 1 e 10, respectivamente em C e
Pascal, no conjunto dos reais.

var a array [1..5] of array [1..10] of real;


var a array[1..5,1..10] of real;

Este tipo de dado é utilizado para representar um numero finito de elementos de um mesmo tipo de
dado e está presente na maioria das linguagens de programação. O componente de um vetor pode
de um tipo de dado elementar com um tipo de dado agregado.

A seleção de um componente do vetor é feita através de indexação, isto é, fornecendo


como índice um valor apropriado no domínio de elementos possíveis do vetor. Assim, por exemplo, o
elemento do vetor “a”, mapeado pelo valor “i” no domínio, seria denotado por “a[i]”.

Linguagens que interpretam um vetor multidimensional como uma composição


de mapeamentos, mas permitem uma definição condensada, obrigam que a seleção de um elemento seja
feita de maneira compatível com a forma de sua definição. Por exemplo, para um vetor com duas
dimensões, o elemento mapeado pelo primeiro índice com um valor “i” especifica a linha e o segundo
com um valor “j” especifica a coluna seria “a[i][j]” ou “a[i,j]”, dependendo de como foi declarado. Deve-
se ter muito cuidado ao fazer a indexação, pois, indexar com um valor fora do domínio resulta em um erro
que usualmente só pode ser detectado em tempo de execução.

Tipo de dado Seqüências ( Strings )

Uma seqüência consiste em um número qualquer de ocorrências ordenadas de dados de um certo tipo.
Este mecanismo de estruturação tem a propriedade importante de deixar em aberto o número de
ocorrências do componente e, portanto, requer que a implementação subjacente seja capaz de armazenar
objetos de tamanho arbitrário. Em outras palavras, objetos criados por seqüência devem possuir um
tamanho variável, ou seja, são dinâmicos. Para evitar o problema da alocação dinâmica de memória
algumas linguagens de programação exigem que seja definido
o tamanho de uma variável string em sua declaração.

Arquivos sequenciais são um exemplo deste mecanismo. Quando uma linguagem de programação faz a
leitura de um arquivo sequencial na verdade está lendo uma sequencia de caracteres. Nestes casos o
tamanho do registro, que deve ser conhecido pelo programador, permite que seja feito o tratamento
adequado a cada registro do arquivo, delimitando o inicio e fim de cada registro e obtendo os valores dos
campos.

Outra implementação usual corresponde aos strings, no qual o tipo do componente é o carácter. Em
outras palavras, strings são apenas vetores de caracteres. Como já foi falado acima, os strings
implementados nas linguagens nem sempre são dinâmicos ou possuem tamanho arbitrário. Muitas
linguagens exigem que em sua definição seja especificado um tamanho, podendo esse tamanho
ser interpretado como o número máximo ou o número constante de caracteres que o string pode ter. A
linguagem Java não exibe que se informe o tamanho de um String e portanto faz uso de alocação
dinâmica, em tempo de execução. Já a linguagem Pascal exige que seja informado o tamanho do String,
fazendo uso de alocação estática, em tempo de compilação.

Programas que fazem uso de alocação dinâmica possuem desempenho ligeiramente inferior ao de
programas que fazem uso apenas de alocação estática. Entretanto, dependendo do projeto do programa,
nem sempre é possível determinar antecipadamente o tamanho de um String e portanto em alguns casos
particulares o uso de alocação dinâmica é necessário para se obter maior flexibilidade no tratamento do
string.

As operações convencionais sobre strings incluem concatenação, seleção de um componente ou extração


de um substring especificando-se as posições do primeiro e do último carácter desejado. A maioria das
linguagens de programação possuem funções embutidas para suporte a estas operações.
A vantagem de se usar uma linguagem fortemente é que qualquer erro de
incompatibilidade de tipo consegue ser verificado em tempo de compilação e pode
portanto ser corrigido antes do programa ser executado. Linguagens fracamente tipadas
dão maior liberdade porém não fazem verificação estática, em tempo de compilação, e
portanto podem ocorrer erros de conversão de tipos em tempo de execução, gerando
problemas inesperados. Na prática erros de conversão de tipo devem ser tratados
adequadamente, veremos isso na próxima aula.

É difícil abstrair um comportamento comum aos exemplos de seqüências fornecidos


pelas LP. Por exemplo, Pascal e C tratam strings como vetores de caracteres, sem operações primitivas
especiais para sua manipulação. Já PL/I e Ada fornecem operações primitivas para manipulação de
strings, mas, para reduzir o problema de alocação dinâmica de memória, exigem que o tamanho máximo
de um string seja especificado na sua declaração

Verificação de Tipos de Dados


Uma linguagem que permite que todos os testes de tipo sejam efetuados estaticamente,
é dita fortemente tipada. Neste caso, o compilador pode garantir a ausência de erros de tipo nos
programas. Java é um exemplo de uma linguagem fortemente tipada, já o Pascal não é uma linguagem
fortemente tipada, pois, por exemplo, em Pascal intervalos e o emprego correto dos registros com
variantes não podem ser testados estaticamente.

As regras de compatibilidade de tipos, por sua vez, devem dar uma especificação exata de como
o mecanismo de testes de tipos deve ser aplicado. É possível definir duas noções de compatibilidade de
tipos:

- Equivalência nominal: duas variáveis possuem tipos compatíveis se possuem o


mesmo nome de tipo, definido pelo usuário ou primitivo, ou se aparecem na mesma declaração.

- Equivalência estrutural: duas variáveis possuem tipos compatíveis se possuem a


mesma estrutura. Para se verificar a equivalência estrutural, os nomes dos tipos definidos pelo
usuário são substituídos pelas suas definições. Este processo é repetido até não sobrarem mais nomes de
tipos definidos pelo usuário. Os tipos são então considerados estruturalmente equivalentes se possuem
exatamente a mesma descrição, isto é, possuem o mesmo número de elementos, com mesmo tipo de dado
para cada elemento e com os elementos definidos na mesma ordem. A Equivalência estrutural é
verificada principalmente para tipos definidos pelo usuário como é o caso de tipos agregados.

Importante! A vantagem de se usar uma linguagem fortemente é que qualquer erro de


incompatibilidade de tipo consegue ser verificado em tempo de compilação e pode portanto ser
corrigido antes do programa ser executado. Linguagens fracamente tipadas dão maior liberdade
porém não fazem verificação estática, em tempo de compilação, e portanto podem ocorrer erros de
conversão de tipos em tempo de execução, gerando problemas inesperados. Na prática erros de
conversão de tipo devem ser tratados adequadamente, veremos isso na próxima aula.

Para ilustrar as regras de compatibilidade de tipos considere o segmento de código a seguir escrito
numa LP hipotética:

type s1 is struct {
int y;
int w;
};

type s2 is struct {
int y;
int w;
};

type s3 is struct {
int y;
};

s3 func(s1 z) { ... };

...
s1 a, x;
s2 b;
s3 c;
int d;
...

Vamos analisar o trecho de código abaixo:

a = b; --(1) // Errado pois a e b são variáveis de tipos diferentes, s1 e s2.


x = a; --(2) // Ok pois são variaveis do mesmo tipo de dado s1

c = func(b); --(3) // Errado pois b é do tipo de dado s2 e no entanto a função func espera um parâmetro do
tipo s1.

d = func(a); --(4) // Errado pois d é do tipo inteiro e portanto é incompatível com o tipo de retorno da
função func que é s3.

Importante! É muito importante sempre se certificar de que se está trabalhando com variaveis de
mesmo tipo ou que seja possível efetuar a conversão de um tipo para o outro sem perda de
informação.

Conversão de Tipos de Dados

Por conta da incompatibilidade de tipos de dado, freqüentemente é necessário converter


um valor de um tipo de dado em um valor de outro tipo de dado, como, por exemplo,
quando se quer somar uma variável inteira à uma constante real. Na maior parte das linguagens essa
conversão é implícita. A conversão implícita é tornada explícita pelo tradutor, que gera
código para conversão baseado no tipo dos operadores e na hierarquia de tipos da linguagem. Esta
conversões automáticas, seguindo a terminologia do Algol 68, são chamadas de coerção.

Entretanto, algumas linguagens permitem apenas a conversão explícita, isto é, o programador deve usar
operadores de conversão.

Em geral, o tipo de coerção que pode ocorrer em um determinado ponto depende do


contexto. Por exemplo, na seguinte instrução em C:

x = x + z;

onde “z” é um float e “x” é um int, há uma coerção de “x” para float para avaliar a
aritmética do operador de soma, e há uma coerção do resultado para int para fazer a atribuição.
Pode-se observar, então, que o C fornece um sistema
simples de coerção. Além disso, conversões explícitas podem ser aplicadas em C
usando o construtor cast. Por exemplo, um cast pode ser usado para sobrescrever a coerção não
desejada que seria aplicada. Assim, o comando acima ficaria:

x = x + (int) z;

A linguagem Ada não fornece coerção. Sempre que uma conversão é permitida através
da linguagem, ela deve ser invocada explicitamente. Por exemplo, se “X” é declarado como uma
variável FLOAT e “I” como um INTEGER, a
atribuição de “X” para “I” pode ser realizada pela seguinte instrução:

I := INTEGER(X);

A função de conversão INTEGER fornecida por Ada processa um inteiro a partir de um


valor de ponto flutuante arredondando para o inteiro mais próximo
Em Java a conversão explícita é realizada por meio de construtores cast, conforme o exemplo abaixo:

float a = (float) 5.0; //Conversão do double 5.0 para float.


int b = (int) 5.1987; //Conversão de double para int. Ocorre
perda de informação pois o tipo double é maior do que o tipo int.

float c = 100; //Conversão de int para float é implícito,


não precisa de casting.

int d = 'd'; //Conversão de char para int é implícito,


não precisa de casting.

Abaixo temos um exemplo de programa em Java que realiza tanto


conversões implícitas quanto conversões explícitas.

/**
* Exemplo de conversão de tipos primitivos utilizando casting.
*/
public class ExemploCasting {

public static void main(String[] args) {

/* Casting feito implicitamente, pois o valor possui um


tamanho menor que o tipo da variavel que irá recebe-lo. */

char a = 'a';
int b = 'b';
float c = 100;

System.out.println(a); //Imprime a
System.out.println(b); //Imprime 98
System.out.println(c); //Imprime 100.0

/* Casting feito explicitamente, pois o valor possui um tamanho


maior que o tipo da variavel que irá recebe-lo. */

int d = (int) 5.1987;

float e = (float) 5.0;

int f = (char) (a + 5);

char g = (char) 110.5;

System.out.println(d); //Imprime 5
System.out.println(e); //Imprime 5.0
System.out.println(f); //Imprime 102
System.out.println(g); //Imprime n

Importante! Toda conversão deve ser verificada pois nem sempre a conversão é
possível. Por isso, as conversões devem ser tratadas a fim de que eventuais erros sejam
adequadamente manipulados pelo programa. Este assunto será visto na próxima aula.

Você também pode gostar