Baixe o app para aproveitar ainda mais
Esta é uma pré-visualização de arquivo. Entre para ver o arquivo original
ExerciciosCap8.pdf Resolução dos Exercícios do Capítulo VIII 1. Explique as vantagens de se possuir um mecanismo de exceções incorporado a LP. Ilustre essas vantagens apresentando exemplos de código com funcionalidade equivalente em C e JAVA. As vantagens de se ter um mecanismo de exceções incorporado à LP são: a melhoria da legibilidade dos programas, pois separam o código com a funcionalidade principal do programa do código responsável pelo tratamento de exceções; o aumento da confiabilidade dos programas, uma vez que normalmente requerem o tratamento obrigatório das exceções ocorridas e porque promovem a idéia de recuperação dos programas mesmo em situações anômalas; e o incentivo a reutilização, redigibilidade e a modularidade do código responsável pelo tratamento (em particular, em linguagens orientadas a objetos, exceções são instâncias de classes, o que faz com que essa parte do programa herde todas as propriedades relativas à reutilização e à modularidade fornecidas pela programação orientada a objetos). Exemplo: // Em C int trata_formatacão_errada (void) { printf (“Formatacao Errada”); return -99; } int lenum () { char s[30]; gets(s); if (isNumber(s)) return atoi(s); else return trata_formatacao_errada(); } int trata_divisão_zero (void) { printf (“Divisão por zero.”); return -98; } int divideInteiros (int numerador, int denominador) { if (denominador == 0) return tratar_divisão_zero (); else return numerador / denominador; } main() { //... int num, den, resultado; num = lenum(); if (num != -99) { den = lenum(); if (den != -99) { 1 resultado = divideInteiros (num, den); if (resultado != -98) { //.. } // Em JAVA // ... int num, den, resultado; try { int num = Integer.valueOf(n).intValue(); int den = Integer.valueOf(d).intValue(); int resultado = num / den; } catch (NumberFormatException e) { System.out.println (e); } catch (ArithmeticException e) { System.out.println (e); } // ... Os trechos de código em C e JAVA, mostrados no exemplo acima, cumprem basicamente a mesma funcionalidade. Ambos lêem duas strings, tentam convertê-las em números, caso tenham sucesso, dividem a primeira pela segunda. Caso não tenham sucesso nas operações de conversão de string para números e de divisão, são mostradas mensagens correspondentes e o programa prossegue a partir do fim do trecho. A abordagem de JAVA é mais legível do que a de C pois existe uma separação do código do programa que implementa a funcionalidade (trecho escrito dentro do boloco try) do código de tratamento de erros. Isso não ocorre em C, uma vez que após a chamada das operações de leitura dos número e divisão é necessário incluir um teste para verificar se ocorreu a condição excepcional. A abordagem de JAVA também é mais confiável porque não requer que o programador lembre, identifique e teste todas as possíveis condições causadoras de exceções. Isso já é feito pelas funções e operações utilizadas (observe em particular que não é necessário testar se o denominador é zero antes da operação de divisão). Além disso, caso o programador se esqueça de tratar as exceções que possam ocorrer em um determinado trecho, o compilador JAVA pode lembrá-lo (em caso de exceções que não sejam RuntimeException ou promover um tipo particular de tratamento, encerrando o programa e imprimindo dados que facilitem a identificação de onde e porque ocorreu a situação excepcional). Por fim, pode-se observar que a abordagem de JAVA é muito mais redigível que a de C. Precisa-se escrever muito menos para se conseguir a mesma funcionalidade. Em parte, isso ocorre pela existência de funções pré-definidas na biblioteca de classes. No entanto, grande parte desta melhor redigibilidade é obtida pela não necessidade de testar as operações antes ou depois de executá-las para verificar a ocorrência de exceções (por exemplo, não é preciso testar duas vezes se a operação de conversão foi bem sucedida, como ocorre no bloco principal em C). Adicionalmente, pode-se ainda se beneficiar da orientação a objetos para reutilizar tratadores de exceções (por exemplo, a função toString das classes NumberFormatException e 2 ArithmeticException é reutilizada no momento de imprimir as mensagens nos tratadores de exceção do exemplo em JAVA). 2. Erros ordinários podem ser tratados no mesmo ambiente no qual foram identificados. Cite vantagens no uso de um mecanismo de exceções como o de JAVA para o tratamento desse tipo de erro. Em JAVA, a ocorrência de erros é sinalizada através de exceções, isto é, objetos especiais que carregam informação sobre o tipo de erro detectado. As vantagens de se utilizar um mecanismo como o de JAVA para tratar erros ordinários são a melhoria da legibilidade dos programas, pois separa o código com a funcionalidade principal do programa do código responsável pelo tratamento de exceções; o aumento da confiabilidade e robustez dos programas, uma vez que normalmente requer o tratamento obrigatório das exceções ocorridas e porque promove a idéia de recuperação dos programas mesmo em situações anômalas; e o incentivo a reutilização e a modularidade do código responsável pelo tratamento (em particular, em linguagens orientadas a objetos, exceções são instâncias de classes, o que faz com que essa parte do programa herde todas as propriedades relativas à reutilização e a modularidade fornecidas pela programação orientada a objetos). 3. Analise o seguinte trecho de programa em C: 3 int leArquivo ( int v [] ) { char *n; int cod; FILE *p; cod = leNomeArq (n); if (cod == 0) { printf ("nome invalido"); return -1; } cod = abreArq (n, p); if (cod == 0) { printf("arquivo inexistente"); return -1; } cod = carregaArq (p, v, 100); if (cod == 0) return -2; cod = fechaArq (p); if (cod == 0) return -3; return 0; } int tentaLer (int v [ ] ) { int cod; do { cod = leArquivo (v); if (cod == -1) { if (!continua ( )) return cod; } else { return cod; } } while (1); } Considere que as funções leNomeArq, abreArq, carregaArq e fechaArq retornam 0 (zero) se não forem bem sucedidas e 1 (um), caso contrário. Considere também que a função continua pergunta ao usuário se ele deseja tentar novamente e retorna 1 (um) em caso afirmativo e 0 (zero) em caso negativo. Refaça esse programa usando o mecanismo de tratamento de exceções de C++. Na versão em C++, as funções leNomeArq, abreArq, carregaArq e fechaArq retornam void mas disparam respectivamente as seguintes exceções nomeExc, arqExc, cargaExc e fechaExc. Compare as duas soluções em termos de redigibilidade e legibilidade, justificando. Programa em C++: void tentaLer () throw (nomeExc, arqExc) { do { try { leNomeArq(n); abreArq(n, p); } catch (nomeExc e) { cout << “nome invalido\n”; if (!continua()) throw e; } catch (arqExc e) { cout << “arquivo inexistente\n”; if (!continua()) throw e; } while (1); } main () { int vet [100]; char *n; FILE *p; try { 4 main ( ) { int cod; int vet [100]; cod = tentaLer (vet); switch (cod) { case 0: break; case -1: printf ("erro de nome"); break; case -2: printf ("erro de carga"); break; case -3: printf("erro de fechamento"); }; ordena (vet); imprime (vet); } tentaLer(); carregaArq (p, v, 100); fechaArq (p); } catch (nomeExc) { cout << “erro de nome\n”; } catch (arqExc) { cout << “erro de nome\n”; } catch (cargaExc) { cout << “erro de carga\n”; } catch (fechaExc) { cout << “erro de fechamento\n”; } ordena (vet); imprime (vet); } No programa escrito em C++ a legibilidade é melhorada, pois o código com a funcionalidade principal do programa é separado do código responsável pelo tratamento de exceções. A redigibilidade é melhorada porque não se necessita utilizar códigos numéricos e testá-los em vários pontos do programa. 4. Considere o seguinte esqueleto de programa em C++: class B { int k; float f; public: void f1() { … try { … throw k; … throw f; … }catch (float){ … } … } } class A { int j; float g; B b; public: void f2() { … try { … try { … b.f1(); 5 … throw j; … throw g; … }catch(int){ … } … }catch (float){ … } … } } main ( ) { A a; … a.f2 ( ); … } Indique, para cada possível exceção disparada, o local onde ela será tratada. • Exceção k → O mecanismo de exceções tentará casar essa exceção com a do tratador do bloco definido na função f1 da classe B. Nesse caso, não haverá casamento e a exceção será propagada para o bloco mais interno definido na função f2 da classe A. O tratador desse bloco será executado. • Exceção f → O mecanismo de exceções tentará casar essa exceção com a do tratador do bloco definido na função f1 da classe B. Nesse caso, haverá casamento e esse tratador será executado. • Exceção j → O mecanismo de exceções tentará casar essa exceção com a do tratador do bloco definido na função f2 da classe A. Nesse caso, haverá casamento e esse tratador será executado. • Exceção g → O mecanismo de exceções tentará casar essa exceção com a do tratador do bloco definido na função f2 da classe A. Nesse caso, não haverá casamento e a exceção será propagada para o bloco mais externo definido na função f2 da classe A. O tratador desse bloco será executado. 5. Explique os mecanismos oferecidos por C, C++ e JAVA para o tratamento de exceções. Enfoque sua explicação na comparação dos seguintes aspectos: a) obrigatoriedade ou não do tratamento de exceções por um usuário de uma função que dispara exceções; b) existência de exceções disparadas pelo próprio mecanismo de exceções da linguagem (tal como quando ocorre divisão por zero). A linguagem de programação C não oferece qualquer mecanismo específico para o tratamento de exceções, ficando a critério do programador implementá-lo ou não. As linguagens C++ e JAVA possuem mecanismo de tratamento de exceções. Enquanto o mecanismo de C++ não obriga o programador a tratar as exceções e não possui exceções disparadas pelo próprio mecanismo de exceções da linguagem, o mecanismo 6 de JAVA oferece vários tipos de exceções detectadas automaticamente e força o programador a tratar grande parte delas. 6. Considere o seguinte trecho de código em JAVA. class InfracaoTransito extends Exception {} class ExcessoVelocidade extends InfracaoTransito {} class AltaVelocidade extends ExcessoVelocidade {} class Acidente extends Exception {} class Defeito extends Exception {} abstract class Dirigir { Dirigir() throws InfracaoTransito {} void irTrabalhar () throws InfracaoTransito {} abstract void viajar() throws ExcessoVelocidade, Defeito; void caminhar() {} } public class DirecaoPerigosa extends Dirigir { DirecaoPerigosa() throws Acidente {} void caminhar() throws AltaVelocidade {} public void irTrabalhar() {} void viajar() throws AltaVelocidade {} public static void main(String[] args) { try { DirecaoPerigosa dp = new DirecaoPerigosa (); dp.viajar (); } catch(AltaVelocidade e) { } catch(Acidente e) { } catch(InfracaoTransito e) { } try { Dirigir d = new DirecaoPerigosa(); d.viajar (); } catch(Defeito e) { } catch(ExcessoVelocidade e) { } catch(Acidente e) { } catch(InfracaoTransito e) {} } } O trecho de código acima apresenta dois erros identificáveis em tempo de compilação. Que erros são esses? Justifique sua resposta. JAVA estabelece a seguinte regra para garantir o uso apropriado do mecanismo de exceções: os construtores devem necessariamente propagar as exceções declaradas no construtor da superclasse. Se o construtor da superclasse pode propagar exceções, o da subclasse também deverá propagá-las pois o último necessariamente chama o primeiro. Assim, o primeiro erro é que o construtor de DirecaoPerigosa não propaga a exceção InfracaoTransito, quando deveria fazê-lo, pois DirecaoPerigosa é subclasse de Dirigir e nesta classe o construtor propaga a exceção InfracaoTransito. 7 O outro erro identificável em tempo de compilação é a implementação do método caminhar na classe DirecaoPerigosa, pois sua implementação dispara a exceção AltaVelocidade, que não está listada na especificação desse método na superclasse Dirigir. O compilador de JAVA impede que isso possa ser feito porque no caso de se chamar o método caminhar de DirecaoPerigosa através de uma referência a superclasse Dirigir, isso poderia ocasionar o disparo da exceção AltaVelocidade, sem que ela fosse devidamente tratada. 7. Apresente as abordagens que linguagens como C podem usar para lidar com erros em situações nas quais não há conhecimento suficiente para tratar o erro no local onde ele ocorre. Essas abordagens devem passar as informações do erro para um contexto mais externo para que ele possa ser tratado. Enumere e explique os problemas com cada uma delas. As abordagens utilizadas são: o retorno do código de erro indicando a exceção ocorrida em uma variável global, no resultado da função ou em um parâmetro específico. A opção de utilizar uma variável global não é muito boa porque o usuário da função pode não ter ciência de que essa variável existe (uma vez que isso não fica explícito na sua chamada) e também porque uma outra exceção pode ocorrer antes do tratamento da anterior, sobrescrevendo o código de retorno da primeira exceção antes dessa ser tratada efetivamente. A opção de usar o resultado da função como código de retorno nem sempre é possível porque pode haver incompatibilidade de valores e de tipo com o resultado normal da função (afinal, o retorno da função normalmente é usado para retornar o resultado da função e não um código). Já a opção de usar um parâmetro para retornar o código de exceção é melhor do que o retorno em variável global ou no resultado da função. Não obstante, ela exige a inclusão de um novo parâmetro nas chamadas dos subprogramas e requer a propagação desse parâmetro até o ponto de tratamento da exceção, diminuindo a redigibilidade do código. Contudo, o grande problema relacionado com essa solução é o fato de a experiência ter mostrado que, na maioria das vezes, o programador que chama a função não testa todos os códigos de retorno possíveis, uma vez que não é obrigatório fazê-lo. 8. Embora o esqueleto de programa JAVA seguinte seja válido sintaticamente, ele não se comporta apropriadamente em uma situação específica (considere que uma operação só é completada se realizada sem ocorrência de exceção). Identifique que situação é essa. Justifique sua resposta. Reformule o programa para que esse problema seja corrigido. public class DefeitoCarro { class SemArranque extends Exception {} class SuperAquecim extends Exception {} public void ligar() throws SemArranque { … if (…) throws SemArranque(); … } public void mover() throws SuperAquecim { … 8 if (…) throws SuperAquecim(); … } public void desligar() {} public static void main(String[] args) { DefeitoCarro c = new DefeitoCarro (); try { c.ligar(); c.mover(); } catch(SemArranque e) { System.out.println("tem de empurrar!!!"); } catch(SuperAquecim e) { System.out.println("vai fundir!!!"); } finally { c.desligar(); } } } A situação ocorre quando a exceção SemArranque é disparada na função ligar. Se essa exceção ocorre, a função ligar não é executada, pois o fluxo do programa é direcionado para o tratador de exceções e, depois para o bloco finally. Nesse caso, a função desligar será executada, embora o carro não esteja ligado. Programa reformulado: public class DefeitoCarro { class SemArranque extends Exception {} class SuperAquecim extends Exception {} public void ligar () throws SemArranque { … if (…) throws SemArranque (); … } public void mover () throws SuperAquecim { … if (…) throws SuperAquecim (); … } public void desligar () {} public static void main (String [] args) { DefeitoCarro c = new DefeitoCarro (); try { c.ligar (); try { c.mover(); } catch (SuperAquecim e) { System.out.println (“vai fundir!!!”); } finally { c.desligar (); } } catch (SemArranque e) { 9 System.out.println (“tem de empurrar!!!”); } } } 9. Ao se compilar o seguinte programa JAVA ocorre um erro de compilação relacionado ao uso de exceções. Identifique qual é esse erro, atentando para o fato que nenhuma exceção disparável no programa é herdeira de RuntimeException, e diga como você o corrigiria. Considerando que o problema foi corrigido tal como você propôs, mostre o que será impresso durante a execução do programa. Mostre o que seria impresso caso o valor atribuído a variável i do método main fosse 2. Mostre, por fim, o que seria impresso se o valor de i fosse 3. class testaExcecoes { public static void main(String[] args) { int i = 1; try { primeiro(i); System.out.println("depois de primeiro"); } catch (NullPointerException e){ System.out.println("trata no primeiro bloco"); } System.out.println("saiu do primeiro bloco"); } public static void primeiro(int i) throws NullPointerException { try { segundo(i); System.out.println("depois de segundo"); } catch (IOException e) { System.out.println("trata no segundo bloco"); } System.out.println("saiu do segundo bloco"); } public static void segundo(int i) throws NullPointerException { try { switch(i) { default: case 1: throw new IOException(); case 2: throw new EOFException(); case 3: throw new NullPointerException(); } System.out.println("depois do switch"); } catch (EOFException e) { System.out.println("trata no terceiro bloco"); } System.out.println("saiu do terceiro bloco"); } } Java exige a especificação das exceções não tratadas nos cabeçalhos dos métodos, as quais não sejam RuntimeException, utilizando a cláusula throws. 10 O erro neste programa é a não identificação da exceção IOException no cabeçalho da função segundo. Ela não é tratada nessa função e também não é uma RuntimeException, portanto, deve ser especificada para que possa ser propagada para o código no qual o método segundo foi chamado. Correção do código: public static void segundo (int i) throws NullPointerException, IOException { ... • Execução para i = 1: trata no segundo bloco saiu do segundo bloco depois de primeiro saiu do primeiro bloco • Execução para i = 2: trata no terceiro bloco saiu do terceiro bloco depois de segundo saiu do segundo bloco depois de primeiro saiu do primeiro bloco • Execução para i = 3: trata no primeiro bloco saiu do primeiro bloco 10. Existem posições controversas com relação a incorporação de um mecanismo rigoroso de tratamento de exceções em LPs. Enquanto alguns defendem o rigor, outros preferem um mecanismo mais flexível. Por exemplo, alguns programadores JAVA são defensores do uso exclusivo das exceções da classe RuntimeException ou de suas subclasses. Indique a característica dessas classes de exceção que justifica essa postura. Apresente argumentos favoráveis e contrários a posição adotada por esses programadores. A classe RuntimeException apresenta um comportamento diferenciado. As exceções dessa classe não necessitam ser tratadas obrigatoriamente pelo programador (o compilador não indica erro quando elas não são tratadas ou propagadas). Se, por um lado, a existência desse tipo de exceções em JAVA poupa o programador de uma grande dose de trabalho (uma vez que ele não necessita fornecer tratadores para essas exceções ou especificar sua propagação), por outro lado, isso torna os programas um pouco menos confiáveis (uma vez que não se exige o tratamento dessas exceções, o programador pode se esquecer de fazê-lo). 11 ExerciciosCap9.pdf Resolução dos Exercícios do Capítulo IX 1. Se a programação concorrente traz dificuldades para a programação, quais vantagens se têm com a sua utilização? Nos dias atuais a programação concorrente está muito presente, seja quando imprimimos um documento e ao mesmo tempo o editamos, seja quando há duas instâncias do mesmo programa em execução. A programação concorrente permite que atividades sejam feitas em menor espaço de tempo e produz uma melhor interatividade entre o sistema e os usuários por conseqüência da multiprogramação. 2. Quais são as principais diferenças entre threads e processos? Cite as respectivas vantagens e desvantagens de sua utilização. Processos são programas em execução, enquanto threads são fluxos de execução em um determinado processo. Processos apresentam estados (novo, executável, em espera, em execução e encerrado), enquanto o estado do thread é definido pelo estado do processo em que ele se encontra. Utilizar apenas processos concorrentes pode levar a um uso exagerado da memória, visto que o estado atual dos registradores e demais atributos devem ser persistidos quando um processo sai do estado "em execução" e passa para o estado "em espera". Além disso, o controle dos processos produz um grande overhead no sistema, reduzindo a performance geral. Threads, por outro lado, possibilitam uma melhor performance e economia de memória. Note que não existem threads sem processos. 3. Quais são as principais diferenças entre memória compartilhada e de troca de mensagens? Cite vantagens e desvantagens. Como o nome já diz, "memória compartilhada" permite um compartilhamento da memória física entre sistemas concorrentes. "Troca de mensagens" faz uso de passagem de mensagem para comunicação entre os sistemas. Trabalhar com programação concorrente fazendo uso de memória compartilhada é mais eficiente que utilizar troca de mensagens. Entretanto, para utilizar memória compartilhada se deve implementar mecanismos que garantam a exclusão mútua no acesso à memória, o que não é trivial. Uma outra vantagem de utilizar troca de mensagens é a possibilidade de utilizar memórias físicas em locais diferentes, ou seja, não é necessário que a memória utilizada fique em apenas um computador. 4. Mostre como é possível utilizar semáforos junto aos laços while dos códigos do produtor e do consumidor no problema mostrado no exemplo 9.2, reduzindo assim o overhead do sistema. Inicialização: 1 fim = 0; 2 ini = 0; 3 n = 0; 4 5 semaforo Cheio; 1 6 Cheio.valor = 1; 7 semaforo Vazio; 8 Vazio.valor = 1; Código do produtor: 1 for (i=0; i<1000; i++) { 2 // while (n == capacidade) ; 3 while (n == capacidade) P(Cheio) 4 buf[fim] = produzir(i); 5 fim = (fim + 1) % capacidade; 6 n++; 7 V(Vazio) 8 } Código do consumidor: 1 for (i=0; i<1000; i++) { 2 // while (n == 0) ; 3 while (n == 0) P(Vazio) 4 consumir(buf[ini]); 5 ini = (ini + 1) % capacidade; 6 n--; 7 V(Cheio) 8 } Com a inclusão de dois semáforos (“Cheio” e “Vazio”) é possível reduzir o overhead do sistema causado pelo uso exclusivo dos laços while nos códigos do produtor e do consumidor. Na linha 3 do código do produtor, caso o buffer esteja cheio, o processo produtor é colocado em espera até que seja consumido algo. Caso contrário, um novo elemento é produzido e o semáforo “Vazio” é liberado (linha 7). Analogamente, no código do consumidor, o processo é colocado em espera caso o buffer esteja vazio. Em caso contrário, um elemento é consumido e o semáforo “Cheio” é liberado. Observe que os laços while substituídos estão comentados na linha 2, tanto do código do produtor quanto do consumidor. 5. Faça uma classe Semaforo em JAVA que implemente as operações P e V de um semáforo. Utilize para isso os métodos wait() e notify(). A classe deve possuir métodos P e V em exclusão mútua (synchronized). class Semaforo { private static int valor = 1; public synchronized void P() throws InterruptedException { valor -= 1; if (valor < 0) { wait(); } } 2 public synchronized void V() throws InterruptedException { valor += 1; if (valor <= 0) { notify(); } } } 6. Implemente uma tarefa Semaforo em ADA utilizando entradas P e V. task Semaforo is entry P; entry V; end Semaforo; task body Semaforo is begin loop accept P; accept V; end loop; end; 7. Suponha que sejam retiradas as chamadas às entradas iniciar de carro1 e carro2 no exemplo 9.13. Indique a opção abaixo com o resultado correto da execução. a) Aparecerá na tela as seguintes mensagens: O carro 1 esta na posicao 0 O carro 2 esta na posicao 0 b) Aparecerá na tela as seguintes mensagens: O carro 1 esta na posicao 0 O carro 1 esta na posicao 1 O carro 2 esta na posicao 0 O carro 2 esta na posicao –1 c) Não aparecerá nada na tela. d) O programa dará erro em tempo de compilação. Letra C. Não aparecerá nada na tela, pois a tarefa Carro espera pela chamada à entrada "iniciar" para prosseguir a execução. Somente após a chamada à entrada "iniciar" é feita a impressão de algo na tela. 8. Implemente o programa do exemplo 9.9 retirando o semáforo, descreva o que acontece e justifique. Ao retirar todos os semáforos começam a aparecer algumas inconsistências. Um exemplo é que o algoritmo pode fazer com que os ponteiros de início e fim do buffer fiquem com indicação incorreta. Isso pode fazer com que as impressões na tela não reflitam as operações reais, podendo indicar que um elemento está sendo produzido mais de uma vez, quando na realidade não está. 3 9. Quais são as principais características das linguagens C, JAVA e ADA relacionadas à programação concorrente? A linguagem C não oferece mecanismos próprios para programação concorrente. É necessário utilizar bibliotecas de funções ou utilizar recurso de chamada de sistema para trabalhar com processos concorrentes. A linguagem JAVA disponibiliza recursos para implementação de threads e oferece mecanismos de sincronização de métodos. A linguagem ADA utiliza-se da definição de módulos concorrentes permitindo a construção de sistemas concorrentes. ADA oferece ainda recursos para sincronização de tarefas através de objetos protegidos ou troca de mensagens. 4 ExerciciosCap7.pdf Resolução dos Exercícios do Capítulo VII 1. Segundo a classificação de Cardelli e Wegner, existem quatro tipos de polimorfismo. Quais desses tipos de polimorfismo existem em C, C++ e JAVA? Mostre exemplos desses tipos de polimorfismo com trechos de código em C, C++ ou JAVA. Identifique o tipo de polimorfismo que ocorre em cada exemplo e explique porque cada um dos trechos de código é polimórfico. Indique ainda se o polimorfismo é ad-hoc ou universal e justifique. Os quatro tipos de polimorfismo, segundo a classificação de Cardelli e Wegner, são: polimorfismo de coerção, polimorfismo de sobrecarga, polimorfismo paramétrico e polimorfismo por inclusão. Os tipos de polimorfismo existentes em C são o de coerção, o de sobrecarga (C embute sobrecarga em seus operadores, mas os programadores não podem implementar novas sobrecargas, além disso, não existe qualquer sobrecarga de programas) e o paramétrico. Os existentes em C++ são o de coerção, o de sobrecarga (adota postura ampla e ortogonal, realizando e permitindo que programadores realizem sobrecarga de subprogramas e operadores), o paramétrico e o por inclusão. Os existentes em JAVA são o de coerção (só admite a realização de coerções para tipos mais amplos), o de sobrecarga (embute sobrecarga em operadores e em subprogramas de suas bibliotecas, mas somente subprogramas podem ser sobrecarregados pelo programador) e o por inclusão. Exemplos: 1.1) Expressão com coerção em C: #include <stdio.h> main () { int a = 1; float b = 2.5, c = 3.5; c = c + b; //c = somafloat (c, b) printf("%.1f", c); //imprime 6.0 c = c + a; //c = somafloat (c, intToFloat (a)) printf("\n%.1f", c); //imprime 7.0 } O trecho acima é polimórfico, porque a instrução c = c + a (na penúltima linha deste exemplo) sugere que o operador + corresponde a uma função capaz de realizar tanto a operação de somar dois valores do tipo float, quanto a operação de somar um float e um int, quando ela só realiza realmente a primeira dessas operações. Antes da operação soma ser chamada, ocorre um chamada implícita a uma função capaz de converter valores do tipo int em valores do tipo float. 1.2) Sobrecarga do operador + em C: #include <stdio.h> 1 main () { int a = 1, b = 2; float c = 1.0, d = 2.0; a = a + b; //a = somaint (a, b) printf("%d", a); //imprime 3 c = c + d; //c = somafloat (c, d) printf("\n%.1f", c); //imprime 3.0 } O trecho acima é polimórfico porque sugere que o operador + representa uma função capaz de realizar tanto a operação de somar dois valores do tipo int, quanto a operação de somar dois valores do tipo float, sendo que, na realidade, cada ocorrência de + invoca operações específicas para cada uma dessas operações. 1.3) Função genérica em C++: template <class T> T identidade (T x) { return x; } class Horario { int h, m, s; } main () { int a; float b; Horario h1, h2; a = identidade (10); b = identidade (10.5); h2 = identidade (h1); } O trecho acima é polimórfico porque parametriza o subprograma identidade com relação ao tipo do elemento sobre o qual ele opera. A função identidade pode ser aplicada a valores de qualquer tipo. Esse tipo é determinado pela combinação do tipo do valor do argumento com o tipo declarado no cabeçalho da função. 1.4) Herança simples em JAVA: public class Liquidificador { protected int velocidade; protected int velocidadeMaxima; public Liquidificador () { velocidade = 0; velocidadeMaxima = 2; } public Liquidificador (int v) { this (); ajustarVelocidadeMaxima (v); 2 } protected void ajustarVelocidadeMaxima (int v) { if (v > 0) velocidadeMaxima = v; } protected void ajustarVelocidade (int v){ if (v >= 0 && v <= velocidadeMaxima) velocidade = v; } public int obterVelocidadeMaxima () { return velocidadeMaxima; } public int obterVelocidade () { return velocidade; } } public class LiquidificadorAnalogico extends Liquidificador { public LiquidificadorAnalogico () { velocidade = 0; } public void aumentarVelocidade () { ajustarVelocidade (velocidade + 1); } public void diminuirVelocidade () { diminuirVelocidade (velocidade – 1); } } public class LiquidificadorDigital extends Liquidificador { public LiquidificadorDigital () { velocidade = 0; } public void trocarvelocidade (int v) { ajustarVelocidade (v); } } public class LiquidificadorInfo { public void velocidadeAtual (Liquidificador l) { System.out.println (“Velocidade Atual: “+ l.obterVelocidade ()); } } Esse trecho é polimórfico porque a utilização da classe Liquidificador permite o tratamento generalizado de todas as suas subclasses. O tratamento generalizado de classes permite escrever programas que podem ser mais facilmente extensíveis, isto é, que podem acompanhar a evolução de uma hierarquia de classe sem necessariamente serem modificados. Por exemplo, na classe LiquidificadorInfo, existe um método capaz de imprimir a velocidade atual de objetos liquidificador os quais podem ser tanto do tipo digital como analógico. Nesta situação, o polimorfismo permite que um simples trecho de código seja utilizado para tratar objetos diferentes relacionados através de seu ancestral comum, 3 simplificando a implementação, melhorando a legibilidade do programa e também aumentando sua flexibilidade. Os polimorfismos dos exemplos 1.1 e 1.2 são de tipo adhoc. O operador + é associado a diferentes trechos de código que atuam sobre diferentes tipos. Para quem lê o código, pode parecer que esse operador denota um único trecho de código polimórfico, atuando sobre elementos de tipos diferentes. Contudo, isso é apenas aparente, uma vez que para utilizar operandos de tipos diferentes numa mesma operação, o operando que não é do tipo esperado deve ser convertido para o mesmo, e para realizar a operação de adição sobre diferentes tipos, diferentes operações, específicas para cada tipo, são chamadas. Os polimorfismos dos exemplos 1.3 e 1.4 são de tipo universal, pois as estruturas de dados incorporam elementos de tipos diversos e um mesmo código pode ser executado e atuar sobre elementos de diferentes tipos. 2. O que são classes abstratas? Quando devem ser usadas e quais as suas vantagens? Quais diferenças existem na definição de classes abstratas em C++ e JAVA? As classes abstratas são classes que não possuem instâncias, mas que possuem membros (as instâncias de suas subclasses não abstratas) e que, portanto, devem ser necessariamente estendidas, ou seja, devem ser herdadas por outras, mais específicas, contendo os detalhes nela não incluídos. Elas normalmente possuem um ou mais métodos abstratos, isto é, métodos declarados, mas não implementados (a implementação dos mesmos é deixada para as suas subclasses) e também podem ter métodos definidos e atributos próprios. De fato, elas podem possuir até construtores, embora eles nunca possam ser chamados para criar instâncias dessa classe (só podem ser chamados no momento da construção das instâncias das subclasses da classe abstrata). As classes abstratas são especialmente úteis quando uma classe, ancestral comum para um conjunto de classes, se torna tão geral a ponto de não ser possível ou razoável ter instâncias dela. As principais vantagens da sua utilização são: a melhoria da organização hierárquica de classes, pelo encapsulamento de atributos e métodos na raiz da estrutura; a promoção de uma maior disciplina na programação, visto que força o comportamento necessário nas suas subclasses (para cada método abstrato em uma classe abstrata, todas as suas subclasses devem implementar esse método ou também devem ser abstratas); e o incentivo ao uso de amarração tardia, permitindo um comportamento mais abstrato e genérico para os objetos. Diferenças entre C++ e JAVA: Em JAVA, é possível, mas não é comum, criar classes abstratas nas quais nenhum método é abstrato. Já em C++, para uma classe ser abstrata, é obrigatório ter pelo menos um método abstrato. Em JAVA, para tornar uma classe abstrata, basta incluir a palavra abstract como prefixo de definição. Em C++, os métodos abstratos devem ter a terminação =0 e devem ser declarados como virtuais, uma vez que seu comportamento terá que ser definido nas subclasses da classe abstrata. 3. Enquanto em C++ somente os métodos precedidos pela palavra virtual utilizam o mecanismo de amarração tardia de tipos, em JAVA todos os métodos empregam este mecanismo. Justifique esta decisão dos criadores dessas linguagens. O mecanismo de amarração tardia de tipos oferece maior flexibilidade para a escrita de código reutilizável, já que possibilita a criação de código usuário com polimorfismo universal, isto é, código usuário capaz de operar uniformemente sobre 4 objetos de tipos diferentes. Por darem preferência à versatilidade, os criadores de JAVA optaram por sempre adotar esse mecanismo. Contudo, a amarração tardia de tipos reduz a eficiência computacional quando comparada com a amarração estática, pois na amarração tardia é necessário manter sempre a cadeia de ponteiros e segui-la em todas as chamadas de métodos, enquanto na estática nada disso é necessário já que o subprograma a ser chamado é definido em tempo de compilação. Assim, os criadores de C++ decidiram adotar uma postura diferente, em que o programador pode decidir se deseja o uso da amarração tardia, identificada pela declaração do método precedida da palavra virtual, ou da amarração estática, identificada pela omissão da palavra virtual, possibilitando ao programador escolher entre versatilidade e eficiência, respectivamente. 4. Linguagens de programação orientadas a objetos podem adotar herança simples ou múltipla. C++, por exemplo, adota herança múltipla. Quais os dois problemas que podem ocorrer quando se adota herança múltipla? Explique-os usando exemplos em C++. Mostre de que forma C++ permite contornar esses problemas. Apresente um exemplo de situação na qual os mecanismos de C++ são inadequados para tratá-los. Os dois problemas que podem ocorrer quando se adota herança múltipla são o conflito entre nomes de classes bases diferentes e a herança repetida. Os conflitos entre nomes ocorrem quando duas ou mais classes bases possuem nomes idênticos (homônimos) de atributos ou métodos. Por exemplo: class Desempenho { int n; float t; public: int numero () { return n; } float tempo() { return t; } }; class Corrida: public Desempenho { float quilometragem; public: void imprime (); }; class Natacao: public Desempenho { float metragem; public: void imprime () }; class Duatlo : public Corrida, public Natação { char prova; } main () { Duatlo atleta; //atleta.imprime; } 5 Caso a linha comentada fosse compilada, seria detectado um erro de ambigüidade, pois não foi especificado de qual classe herdada éo método referenciado pelo objeto da classe herdeira. Em C++ esse problema pode ser resolvido pela sobrescrição da operação de impressão na classe Duatlo e pelo uso do operador de resolução de escopo ::, como mostrado abaixo, para identificar qual operação de impressão está sendo chamada. class Duatlo: public Corrida, public Natação { char prova; public: void imprime (); }; void Duatlo :: imprime () { if (prova == ‘C’) Corrida :: imprime (); else Natacao :: imprime (); } main () { Duatlo atleta; atleta.imprime; } O problema da herança repetida ocorre quando uma classe faz herança múltipla de classes descendentes de uma mesma classe. Os atributos dessa classe comum são repetidos na classe na qual é feita herança múltipla. C++ fornece um mecanismo especial, utilizando a palavra virtual para resolver esse problema. Se a especificação da classe herdada é precedida por virtual, somente um objeto daquela classe comporá o objeto das classes herdeiras, independentemente de a classe ser herdada múltiplas vezes ou não. No exemplo anterior, a palavra virtual não foi utilizada na especificação das classes Corrida e Natação, o que faz com que os atributos numero (n) e tempo (t) de Desempenho sejam repetidos na classe Duatlo. Isso não ocorre na implementação mostrada a seguir: class Desempenho { int n; float t; public: int numero () { return n; } float tempo() { return t; } }; class Corrida: virtual public Desempenho { float quilometragem; public: void imprime (); }; class Natacao: virtual public Desempenho { float metragem; public: 6 void imprime () }; Contudo, esses mecanismos adotados por C++ para solucionar esses problemas apresentam dois inconvenientes. Primeiro, a especificação da palavra virtual não é feita na classe na qual ocorre a herança repetida. Inicialmente, ao se criar as classes Corrida e Natacao, não se sabe se elas serão herdadas futuramente por uma mesma subclasse e muito menos se esta subclasse precisará ou não compartilhar os atributos comuns. O segundo problema é que não é possível especificar que um atributo deva ser repetido e outro não. No exemplo acima, o número do atleta é o mesmo, mas os tempos para as provas de corrida e natação são tempos distintos. 5. C++ oferece três mecanismos distintos para permitir a realização de estreitamento. Mostre exemplos do uso desses três mecanismos e os compare em termos de confiabilidade e eficiência. As três maneiras distintas oferecidas por C++ para permitir a realização de estreitamento são: o mecanismo usual de conversão explícita (cast), o mecanismo de conversão explícita estática (static_cast) e o mecanismo de conversão explícita dinâmica (dynamic_cast). Por exemplo: class UmaClasse { public: virtual void temVirtual () {}; } class UmaSubclasse: public UmaClasse {} class OutraSubclasse: public UmaClasse {} class OutraClasse {} main () { //primeira parte do exemplo UmaClasse *pc = new UmaSubclasse; OutraSubclasse *pos = dynamic_cast <OutraSubclasse *> (pc); UmaSubclasse *ps = dynamic_cast <UmaSubclasse *> (pc); //segunda parte do exemplo UmaSubclasse us; pc = &us; pc = static_cast <UmaClasse *> (&us); OutraClasse *poc = (OutraClasse *) pc; } Na primeira parte do exemplo acima é mostrado o uso do mecanismo de dynamic_cast. Essa é uma forma de fazer estreitamento de modo seguro. Ao usar dynamic_cast, o que se está tentando fazer é um estreitamento para um tipo particular. O valor de retorno dessa operação será um ponteiro para o tipo desejado, no caso de o estreitamento ser apropriado. De outra forma, o valor retornado será zero (null) para indicar que o tipo não era o esperado. Somente podemos usar o dynamic_cast em classes com funções virtuais. Isso ocorre porque o dynamic_cast usa a informação armazenada em uma tabela de métodos virtuais para determinar o tipo atual. No exemplo, o ponteiro pos receberá zero, pois o 7 estreitamento para OutraSubclasse * é incorreto. É responsabilidade do programador, verificar se o resultado do estreitamento por dynamic_cast é diferente de zero. A operação de dynamic_cast sobrecarrega um pouco a execução do programa, portanto, se um programa usa muito dynamic_cast, isso poderá diminuir a eficiência de execução. O mecanismo de static_cast deve ser utilizado quando é possível saber durante a própria redação do programa com qual tipo estamos lidando no local do estreitamento, pois assim a verificação é feita em tempo de compilação. Usar static_cast para realizar estreitamento é melhor do que o mecanismo de conversão explícita formal (cast), pois o primeiro não permite fazer conversões fora da hierarquia de classes, o que é permitido pelo segundo. Como a eficiência é a mesma para códigos gerados usando ambos os mecanismos, static_cast deve ser preferido, pois é mais seguro. A segunda parte do exemplo mostra o uso desse mecanismo. Nessa parte, um objeto (us) de UmaSubclasse é criado e é feita uma ampliação a um ponteiro para UmaClasse. Essa mesma operação é repetida usando static_cast. Como se trata de uma ampliação, não existe obrigatoriedade de usar static_cast, mas seu uso pode ser conveniente para tornar mais explícita a ampliação e para evitar a realização equivocada de conversões fora da hierarquia de classes. Após as operações de ampliação, o exemplo mostra a diferença entre se fazer estreitamento com static_cast e o mecanismo tradicional. Com o mecanismo tradicional, é possível fazer a conversão fora da hierarquia de classes entre os ponteiros para UmaClasse e para OutraClasse. Isso não é permitido no caso do static_cast. Caso a linha de código na qual essa operação é feita não estivesse comentada, ocorreria um erro de compilação. Resumindo, é boa prática usar preferencialmente os mecanismos do dynamic_cast, pois embora seja mais rápido fazer estreitamento estaticamente, a conversão dinâmica é mais segura, pois os mecanismos de conversão estática podem produzir conversões inapropriadas. 6. Amarração tardia de tipos é o processo de identificação em tempo de execução do tipo real de um objeto. Esse processo pode ser utilizado para a identificação dinâmica do método a ser executado (quando ele é sobrescrito) e para a verificação das operações de estreitamento. Explique como pode ser implementado o mecanismo de amarração tardia de tipos em linguagens como C++ e JAVA e como ele é usado para realizar as operações mencionadas na frase anterior. Em JAVA, na utilização desse mecanismo, quando um método é invocado por uma variável (essa variável aponta para um objeto), uma cadeia de ponteiros é seguida. Essa cadeia começa pela variável que chamou o método e que está na base da pilha de registros de ativação e que aponta para um objeto. Esse objeto apontado pela variável possui, além dos atributos de sua classe, uma referência à tabela de métodos de sua classe. Essa tabela, por sua vez, também possui uma referência à tabela de métodos de sua superclasse. Assim, essa cadeia é seguida até chegar à primeira tabela de métodos da classe na qual esse método foi definido e o método a ser utilizado é identificado. Com relação à verificação das operações de estreitamento, a cadeia de ponteiros citada acima é seguida e, caso a classe para a qual se deve fazer a conversão seja encontrada em algum momento, a operação é validada. 8 7. Tanto C++ quanto JAVA oferecem bibliotecas de classes que disponibilizam estruturas de dados genéricas, tais como listas, árvores e tabelas hash. Ambas utilizam polimorfismo para a implementação dessas estruturas, embora sejam formas diferentes de polimorfismo. Explique como essas linguagens usam o polimorfismo para a implementação dessas estruturas. Discuta as vantagens e desvantagens de cada abordagem. Justifique também porque os criadores dessas linguagens adotaram essa postura diferenciada. Estruturas de dados genéricas são capazes de armazenar e operar sobre elementos de tipos diferentes. Estruturas genéricas podem ser preenchidas com elementos de um mesmo tipo (nesse caso são chamadas de estruturas homogêneas) ou de tipos diferentes (nesse caso são chamadas de estruturas heterogêneas). C++ utiliza o polimorfismo paramétrico proporcionado pelo mecanismo de template para a criação de estruturas de dados genéricas homogêneas, e o mecanismo de polimorfismo por inclusão para a criação de estruturas de dados heterogêneas. É possível ainda combinar o mecanismo de template com o polimorfismo de inclusão para criar estruturas de dados genéricas heterogêneas. JAVA utiliza o polimorfismo por inclusão para permitir a criação de estruturas de dados genéricas heterogêneas. Para isso, JAVA considera todas as classes existentes como subclasses (diretas ou indiretas) da classe Object. Assim, estruturas de dados cujos elementos são do tipo Object podem abrigar elementos de qualquer classe em JAVA. Para ter uma estrutura de dados homogênea, o programador deve garantir que os elementos inseridos sejam sempre de um mesmo tipo. Isso é pior do que a solução de C++, na qual o compilador garante a homogeneidade da estrutura. Além disso, a solução de JAVA obriga a realização de estreitamento sempre que um elemento deve ser acessado a partir da lista. Por outro lado, a solução de JAVA simplifica a linguagem, pois não é necessário incluir o polimorfismo paramétrico, imprescindível em C++. 8. Implemente o tipo abstrato de dados lista genérica em C, C++ e JAVA. É suficiente apresentar a definição do tipo e o cabeçalho dos métodos de construção, ordenação, destruição, verificação de lista vazia, inclusão e exclusão de elemento (não é preciso codificar os métodos da lista). Atente para o fato de que a operação de ordenação na lista deve ser única, mas deve permitir que a lista seja ordenada por critérios distintos. Justifique a sua implementação, enfocando o modo como se obtém a generalidade da lista e o funcionamento da operação de ordenação. //C typedef struct no { void *info; struct no *prox; }No; typedef struct Lista { No *prim, *ult; int tam; }Lista; Lista InicLista (); Lista OrdenaLista (Lista lst, int (*compara) (void*, void*)); void DestroiLista (Lista lst); int VaziaLista (Lista lst); 9 Lista InsereLista (Lista lst, int pos, void *elem); Lista ElimLista (Lista lst, int pos); //C++ template <class T> class Lista { struct no { T info; struct no *prox; }No; No *prim, *ult; int tam; public: Lista (); Lista OrdenaLista (int (*compara) (T, T)); int VaziaLista (); Lista InsereLista (int pos, T elem); Lista ElimLista (int pos); void DesalocaInfos(); ~Lista (); } //JAVA class Lista { private Object [] lst; private int tam = 0; private int marcador; Lista (); public int VaziaLista (); public void OrdenaLista (Comparator c, int ordem); public void InsereLista (int pos, Object o); public void ElimLista (int pos); public void finaliza (); } A generalidade da lista é obtida pela utilização de void* em C, template em C++ e Object em JAVA, que permitem generalizar os elementos que compõem a lista. No caso de C, o tipo void* permite que o elemento da lista seja de qualquer tipo ponteiro, o que confere generalidade a lista. No caso de C++, o mecanismo template possibilita que o tipo do elemento da lista seja definido no momento de criação das variáveis lista. No caso de JAVA, como toda classe em JAVA é subclasse de Object, é possível colocar qualquer tipo de objeto como elemento da lista. A operação de ordenação pode ser realizada em diferentes contextos, isto é, diferentes características dos objetos podem ser comparadas ou pode ser escolhido se o desejado é a ordenação crescente ou decrescente, por exemplo. Isso é feito em C e C++ pela utilização do ponteiro para função compara passado como parâmetro para a função OrdenaLista e pelo parâmetro c da interface Comparator passado para a função OrdenaLista em JAVA. No caso de C e C++, o que se precisa fazer é criar uma função de comparação que compare dois elementos do tipo da lista e passar seu endereço como argumento para o parâmetro formal compara da função OrdenaLista. 10 No caso de JAVA é preciso criar uma classe que implemente a interface Comparator e que defina como deve ser feita a comparação em seu único método. Depois, basta passar um objeto dessa classe para o método OrdenaLista. 9. Em alguns problemas pode ser conveniente permitir a um mesmo objeto participar de duas ou mais listas cujos elementos são de tipos diferentes. Por exemplo, em um problema no qual é necessário armazenar em listas os diversos tipos de dependências de um apartamento haveria uma lista para quartos e outra para salas. Contudo, é possível haver uma mesma dependência usada como quarto e como sala. Nesse caso, essa dependência participaria tanto da lista de quartos quanto da lista de salas. a) Como você resolveria esse problema em uma linguagem que não possui mecanismos para a realização de subtipagem múltipla, ou seja, que não permita a um mesmo objeto fazer parte de listas de dados cujos elementos sejam de tipos distintos. Existe alguma desvantagem na sua solução? b) Sabendo que C também não permite a realização de subtipagem múltipla, responda se é possível resolver esse mesmo problema de outra maneira? Se a resposta for positiva, explique como seria essa solução e faça uma análise de suas vantagens e desvantagens. c) Mostre através da implementação desse exemplo como C++ e JAVA permitem a realização de subtipagem múltipla. Compare ainda os mecanismos oferecidos por C++ e JAVA para realização de subtipagem múltipla apresentando vantagens e desvantagens de cada um. a) Uma forma seria através da existência de um objeto do tipo sala e outro do tipo quarto referindo-se ao mesmo cômodo. O problema com essa abordagem é que qualquer alteração feita em um atributo comum dos objetos deve implicar na atualização de um ou outro objeto, sob pena de se gerar uma inconsistência de dados caso isso não seja feito apropriadamente. b) Em C isso poderia ser feito através da criação de uma nova estrutura representando objetos do tipo sala e quarto juntamente com a especificação do elemento das duas listas como ponteiro para void. Um problema dessa abordagem é que se torna necessário manter junto com o elemento uma indicação de seu tipo para que toda vez que for utilizado se possa verificar o tipo real do elemento. Outro problema é que as listas passam a ser genéricas, podendo assim receber elementos de outros tipos e não apenas salas e quartos. c) C++: class Quarto { public: Quarto() {} int numeroCamas () {} }; class Sala { public: Sala() {} int numeroMesas () {} }; 11 class QuartoSala: public Quarto, public Sala { }; JAVA: interface Quarto { int numeroCamas (); } interface Sala { int numeroMesas (); } class QuartoSala implements Quarto, Sala { public int numeroCamas() {} public int numeroMesas() {} } C++ permite herança múltipla de classes, o que torna muito natural o desenvolvimento de heterarquia de classes e de programas que as utilizam. Por outro lado, o mecanismo de herança de C++ admite a ocorrência dos problemas de conflito de nomes e herança repetida. JAVA só permite herança múltipla de interfaces (isto é, classes abstratas puras). Enquanto isso garante a inexistência de problemas de conflito de nomes e herança repetida, a reutilização de código por herança não é possível (uma vez que interfaces não implementam métodos nem possuem atributos) ou se torna mais complexa (há uma forma de reutilizar código por composição). 10. Considere as seguintes definições de funções e classes em C++: template <class T > T xpto (T x, T y) { return y; } template <class T, class U> U ypto (T x, U y) { return y; } template <class T, class U> T zpto (T x, U y) { return ((T) y); } class tdata { int d, m, a; }; class thorario { int h, m, s; }; class tdimensao { int h, l, w; }; Indique quais das linhas de código de main são legais e explique as que não são. main () { tdata a; 12 thorario b; tdimensao c; a = xpto (a, a); b = xpto (a, a); c = xpto (a, b); a = ypto (a, a); b = ypto (a, a); b = ypto (a, b); c = ypto (a, b); a = zpto (a, a); b = zpto (a, a); a = zpto (a, b); c = zpto (a, b); } Explique ainda como o compilador C++ implementa o mecanismo de polimorfismo paramétrico. Discuta essa solução em termos de reusabilidade de código. Linhas legais: a = xpto (a, a); a = ypto (a, a); b = ypto (a, b); a = zpto (a, a); Linhas que não são legais: b = xpto (a, a); → Uma vez que não foi feita uma sobrecarga do operador de atribuição da classe thorario que receba um parâmetro do tipo tdata, a chamada de xpto geraria erro de compilação porque ela retorna um valor do tipo tdata, mas a atribuição demanda um thorario. c = xpto (a, b); → Uma vez que não foi feita uma sobrecarga do operador de atribuição da classe tdimensao que receba um parâmetro do tipo thorario, a chamada de xpto geraria erro de compilação porque ela retorna um valor do tipo thorario, mas a atribuição demanda um tdimensao. b = ypto (a, a); → Uma vez que não foi feita uma sobrecarga do operador de atribuição da classe thorario que receba um parâmetro do tipo tdata, a chamada da função ypto geraria erro de compilação porque ela retorna um valor do tipo tdata, mas a atribuição demanda um thorario. c = ypto (a, b); → Uma vez que não foi feita uma sobrecarga do operador de atribuição da classe tdimensao que receba um parâmetro do tipo thorario, a chamada da função ypto geraria erro de compilação porque ela retorna um valor do tipo thorario, mas a atribuição demanda um tdimensao. b = zpto (a, a); → Uma vez que não foi feita uma sobrecarga do operador de atribuição da classe thorario que receba um parâmetro do tipo tdata, a chamada da função zpto geraria erro de 13 compilação porque ela retorna um valor do tipo tdata, mas a atribuição espera um thorario. a = zpto (a, b); → A chamada da função zpto geraria erro de compilação porque não existe um construtor definido na classe tdata que receba como parâmetro um objeto thorario. c = zpto (a, b) → A chamada da função zpto geraria erro de compilação porque não existe um construtor definido na classe tdata que receba como parâmetro um objeto thorario e porque ela retorna um valor do tipo tdata, mas a atribuição espera um tdimensao. C++ usa o mecanismo de template para incorporar o polimorfismo paramétrico. A forma de implementação do mecanismo template é curiosa. Ao contrário do que seria mais desejado (a reutilização de código-fonte e objeto), esse mecanismo só possibilita a reutilização de código-fonte. Isso significa que não é possível compilar o código usuário das funções ou classes definidas com polimorfismo paramétrico separadamente do código de implementação dessas funções ou classes. De fato, para compilar funções ou classes paramétricas, o compilador C++ necessita saber quais tipos serão associados a elas. A partir de uma varredura do código usuário, o compilador identifica os tipos associados a essas funções e classes e replica todo o código de implementação para cada tipo utilizado, criando assim um código objeto específico para cada tipo diferente utilizado. 14 11. Cada um dos programas seguintes, escritos em C++, utiliza um tipo de polimorfismo visto nesse capítulo. Defina o que é polimorfismo. Descreva as características desses tipos de polimorfismo, indicando o tipo empregado por cada programa. Execute os programas passo a passo, mostrando o resultado apresentado, indicando onde ocorre polimorfismo e explicando sua execução. O polimorfismo em LPs se refere à possibilidade de criar código capaz de operar (ou, pelo menos, aparentar operar) sobre valores de tipos distintos. Programa 1 15 // programa 1 #include <iostream> class base { public: virtual void mostra1() { cout << "base 1\n"; } void mostra2 () { cout << "base 2 \n"; } }; class derivada1: public base { public: void mostra1() { cout << "derivada 1\n"; } }; class derivada2: public base { public: void mostra2 () { cout << "derivada 2 \n"; } }; void prt(base *q) { q->mostra1(); q->mostra2(); } void main( ) { base b; base *p; derivada1 dv1; derivada2 dv2; p = &b; prt(p); dv1.mostra1(); p = &dv1; prt(p); dv2.mostra2(); p = &dv2; prt(p); } // programa 2 #include <iostream> class teste { int d; public: teste () { d = 0; cout << "default \n"; } teste (int p, int q = 0) { d = p + q; cout << "soma \n"; } teste (teste & p) { d = p.d; cout << "copia \n"; } teste & operator = (teste & p) { cout << "atribuicao 1\n"; d = p.d; return *this; } teste & operator = (int i) { cout << "atribuicao 2\n"; d = i; return *this; } void mostra ( ) { cout << d << " \n"; } }; void main ( ) { teste e1 (2, 6); e1.mostra ( ); teste e2; e2.mostra ( ); teste e3 (73); e3.mostra ( ); teste e4; e4 = e1; e4.mostra(); teste e5 (e2); e5.mostra ( ); e5 = 55; e5.mostra ( ); teste e6 = e3; e6.mostra ( ); teste e7 (21,3); e7.mostra ( ); e1 = e3 = e5 = e7; e1.mostra(); e5.mostra(); } // programa 3 #include <iostream> template <class T> class pilha { T* v; T* p; public: pilha (int i) { cout << "cria "<< i << "\n"; v = p = new T[i]; } ~pilha () { delete[ ] v; cout << "tchau \n"; } void empilha (T a) { cout << "emp "<< a << "\n"; *p++ = a; } T desempilha () { return *--p; } int vazia() { if (v == p) return 1; else return 0; } }; void main ( ) { pilha<int> p(40); pilha<char> q(30); p.empilha(11); q.empilha('x'); p.empilha(22); q.empilha('y'); p.empilha(33); do { cout << p.desempilha() << "\n"; } while (!p.vazia()); do { cout << q.desempilha() << "\n"; } while (!q.vazia()); } Tipo de polimorfismo: Por inclusão. Baseia-se na noção de hierarquia de classes para tratar objetos de um determinado tipo (da superclasse) como sendo de outro (da subclasse) através da amarração dinâmica de tipos. Os subprogramas mostra1 e mostra2 definidos inicialmente na classe base foram reescritos nas suas subclasses derivada1 e derivada2, respectivamente. Resultado da execução: base 1 base 2 derivada 1 derivada 1 base 2 derivada 2 base 1 base 2 Em main são criados inicialmente um objeto do tipo base (b), um ponteiro para objeto do tipo base (*p), um objeto do tipo derivada1 (dv1) e um objeto do tipo derivada2 (dv2). Com a execução da instrução p = &b;, p passa apontar para b. Assim, quando prt (p); é chamada, são executadas as funções mostra1 e mostra2 definidas na classe base, imprimindo, respectivamente, base 1 e base 2. Com a execução de dv1.mostra1();, a função mostra1 definida na classe derivada1 é executada, imprimindo derivada 1. Com a execução de p = &dv1;, p passa a apontar para dv1. Na próxima linha, a execução de prt (p); chama o método mostra1 de derivada1 porque o método mostra1 é amarrado dinamicamente em base (uso da palavra virtual) e, em seguida, chama o método mostra2 de base uma vez que esse método é amarrado estaticamente (não é precedido pela palavra virtual). Assim, são impressos derivada 1 e base 2. A instrução seguinte dv2.mostra2 (); imprime derivada 2. A execução de p = &dv2; faz com que p passe a apontar para dv2. A execução de prt (p); chama o método mostra1 herdado de base por derivada2 e depois chama o método mostra2 de base, uma vez que ele é amarrado estaticamente. Por conseguinte, são impressos base1 e base2. Programa 2 Tipo de polimorfismo: Sobrecarga. Permite várias definições de funções referidas por um mesmo símbolo ou identificador. Dá a aparência de usar na chamada um mesmo trecho de código para diferentes tipos de dados dos parâmetros. Esse programa utiliza o polimorfismo de sobrecarga para implementar diferentes construtores para a classe teste, além de sobrecarregar o operador =. Resultado da execução: soma 8 default 0 soma 16 73 default atribuicao 1 8 copia 0 atribuicao 2 55 copia 73 soma 24 atribuicao 1 atribuicao 1 atribuicao 1 24 24 Em main, a execução de teste e1 (2,6); imprime soma, pois o construtor definido com dois parâmetros na classe teste é chamado. A execução de e1.mostra (); imprime 8. A execução de teste e2; chama o construtor default (sem parâmetros) definido em teste, imprimindo default. A execução de e2.mostra (); imprime 0. A execução de teste e3 (73); imprime novamente soma pois um dos parâmetros do construtor com dois parâmetros assume o valor default 0. A execução de e3.mostra (); imprime 73. A execução de teste e4; imprime novamente default e a execução de e4 = e1 chama o método de atribuição que recebe um teste como parâmetro, imprimindo atribuicao 1. A execução de e4.mostra (); imprime 8. A execução de teste e5 (e2); chama o construtor que recebe um teste como parâmetro e imprime copia, pois o construtor de cópia é chamado. A execução de e5.mostra (); imprime 0. A execução de e5 = 55; chama o operador de atribuição que recebe um inteiro como parâmetro, imprimindo atribuicao 2 e a execução de e5.mostra (); imprime 55. A execução de teste e6 = e3; chama novamente o construtor de cópia, imprimindo copia, já que esta é uma operação de inicialização. A execução de e6.mostra (); imprime 73. A execução de teste e7 (21, 3); imprime novamente soma ie a execução de e7.mostra (); imprime 24, a execução de e1 = e3 = e5 = e7; chama sucessivamente o operador de atribuição que recebe um teste como parâmetro e imprime três vezes atribuicao 1. A execução de e1.mostra (); imprime 24 e a execução de e5.mostra (); também imprime 24. Programa 3 Tipo de polimorfismo: Paramétrico. Permite a definição de estruturas de dados ou funções que contenham um ou mais parâmetros indicando o(s) tipo(s) do(s) elemento (s) que serão manipulados. O mecanismo de template é utilizado para permitir a implementação do tipo pilha genérica. Na execução do programa serão criadas a pilha do tipo inteiro e a pilha do tipo caractere, a partir da mesma classe pilha. Resultado da execução: cria 40 17 cria 30 emp 11 emp x emp 22 emp y emp 33 33 22 11 y x tchau tchau Em main, a instrução pilha<int> p(40); chama o construtor de pilha e imprime cria 40, a instrução pilha<char> q(30); chama o construtor de pilha e imprime cria 30, a instrução p.empilha(11); imprime emp 11, a instrução q.empilha(‘x’); imprime emp x, a instrução p.empilha(22) imprime emp 22, a instrução q.empilha(‘y’); imprime emp y e a instrução p.empilha(33) imprime emp 33. O do-while seguinte imprime inicialmente o número 33 porque é o valor retornado por p.desempilha(), na segunda execução imprime 22 e na terceira imprime 11. O próximo do-while imprime inicialmente a letra y porque é o elemento retornado por q.desempilha() e na segunda execução imprime x. Antes de terminar o programa, o destrutor de p e q são chamados imprimindo tchau duas vezes. 12. Qual a diferença entre as posturas adotadas por JAVA e C++ em relação ao polimorfismo de sobrecarga? Qual dessas posturas você acha melhor? Apresente argumentos justificando sua posição. A diferença é que JAVA embute sobrecarga em operadores e em subprogramas de suas bibliotecas, mas somente subprogramas podem ser sobrecarregados pelo programador, enquanto C++ realiza e permite que programadores realizem sobrecarga tanto de programas quanto de operadores. Os criadores de JAVA resolveram não incluir a sobrecarga de operadores por considerá-la capaz de gerar confusões e aumentar a complexidade da LP. A postura adotada por C++ é mais ampla e ortogonal, facilitando a leitura e redação dos programas, se utilizada corretamente. Em minha opinião pessoal, considero a postura de C++ mais adequada em termos de elegância. Em termos práticos, acho que o ganho conferido é pequeno se comparado a complexidade associada ao uso de sobrecarga de operadores. 13. Uma loja especializada em produtos de arte vende livros, discos e fitas de vídeo. Ela necessita montar um catálogo com as informações sobre cada produto. Todo produto possui um número único de registro, um preço e uma quantidade em estoque. Além disso, deve-se saber o número de páginas dos livros, o número de músicas dos discos e a duração da fita de vídeo. Outro aspecto importante a ser considerado é a existência de um tipo de produto de venda combinada (uma fita de vídeo combinada com um disco). Utilize C, C++ e JAVA para: a) Especificar tipos abstratos de dados para cada um dos produtos da loja (basta apresentar a definição do tipo e os cabeçalhos de suas operações de criação, leitura, obtenção de dados, escrita e destruição). 18 b) Supondo que os produtos da loja estão armazenados em uma lista genérica, fazer uma função/método para receber a lista dos produtos e uma quantidade mínima de produtos a serem mantidos em estoque, e listar quais produtos necessitam de reposição. c) Fazer uma função/método para receber a lista dos produtos, uma quantidade de músicas e uma duração mínima de filme, e listar quais discos possuem menos músicas que a quantidade especificada e quais filmes possuem maior duração que a especificada. AINDA A SER FEITA. 14. O Ministério da Defesa te contratou para desenvolver um protótipo de um sistema de informação em C++ que cadastre os militares existentes nas Forças Armadas Brasileiras e gere duas listagens. a. Crie uma classe abstrata Militar com um atributo inteiro representando sua matrícula e outro atributo representando sua patente. Garanta que todas subclasses concretas de Militar implementem obrigatoriamente os métodos de leitura de dados de um militar, impressão de dados de um militar e verificação se o militar está habilitado para progredir na carreira. Utilize a classe seguinte para representar a patente do militar. class Patente { private: string titulo; int tempo; // na patente public: le() { cin >> titulo; cin >> tempo; } string retornaPatente() { return titulo; } int retornaTempo() { return tempo; } void incrementa (int t) { tempo+=t; } void imprime() { cout << titulo << “ – “ << tempo; } } b. Implemente uma subclasse concreta de Militar, denominada MilitarAeronautica, que será usada para representar os militares dessa divisão das forças armadas. Essa classe possui como atributo adicional o número de horas de vôo efetuadas pelo militar. Note que um militar da aeronáutica está em condições de progredir se tem mais de 2 anos naquela patente e se acumulou mais de 100 horas de vôo nesse período. c. Considerando a existência de duas outras subclasses de Militar semelhantes a MilitarAeronáutica, denominadas MilitarExercito e MilitarMarinha, utilize uma classe ListaMilitares (consistindo de uma lista de ponteiros para militares) para implementar um programa que: • solicite ao usuário o número total de militares das Forças Armadas; 19 • solicite ao usuário a corporação e os dados de cada militar das Forças Armadas; • apresente os dados de todos os militares em condições de progredir na carreira; • apresente os dados de todos os militares da Aeronáutica. Observação: Você não deve implementar a classe ListaMilitares. Considere que, além de possuir o método construtor default e o destrutor, ela também possui os seguintes métodos: void incluir(Militar* m); // inclui um militar ao final da lista Militar* retornar(int i); // retorna o militar na i-ésima posição a) class Militar { int matricula; Patente p; public: virtual void leitura () = 0; virtual void impressao () = 0; virtual int verificacao () = 0; } b) class MilitarAeronautica: public Militar { float horas; public: void leitura () { cin >> matricula; p.le(); cin >> horas; } void imprime () { cout << matricula; p.imprime(); cout << horas; } int verifica () { if (p.retornaTempo() > 2 && horas > 100) return 1; else return 0; } } c) AINDA A SER FEITA. 20 ExerciciosCap6.pdf Resolução dos Exercícios do Capítulo VI 1. Implemente uma função sem parâmetros em C na qual se efetue a troca de dois valores. Utilize-a em um programa executor de trocas de valores entre diversos pares de variáveis. Explique porque os problemas de redigibilidade, legibilidade e confiabilidade seriam ainda mais graves nesse caso do que no exemplo 6.3. int a, b; // variaveis globais void troca(){ int aux; aux = a; a = b; b = aux; } main(){ int c, d, e, f; c = 0; d = 1; e = 2; f = 3; a = c; b = d; troca(); c = a; d = b; a = e; b = f; troca(); e = a; f = b; } Como no exemplo 6.3, a função troca não possui parâmetros, sendo necessário utilizar as variáveis globais a e b para lhe conferir generalidade e possibilitar o seu reuso em main. Os problemas de redigibilidade, legibilidade e confiabilidade são ainda mais graves dos que os do exemplo 6.3 por causa da necessidade de modificação dos valores das variáveis globais c, d, e e f após a execução da função troca. Com isso, torna-se necessário escrever mais, a funcionalidade do programa fica ainda mais obscurecida por envolver uma maior quantidade de atribuições e aumenta-se a possibilidade do programador realizar alguma atribuição indevida ou esquecer alguma atribuição. 2. É possível implementar, para cada tipo primitivo, funções em JAVA nas quais sejam trocados os valores dos seus parâmetros formais? Caso sua resposta seja afirmativa, implemente uma dessas funções e explique como funciona, destacando como a troca é feita. Em caso de resposta negativa, justifique. Existiria alguma diferença na sua resposta caso a troca fosse realizada entre parâmetros de um mesmo tipo objeto? Justifique. Não. Java optou por fazer a passagem de tipos primitivos sempre por cópia para o parâmetro formal, o que impede que uma troca entre os parâmetros formais se reflita nos parâmetros reais. Não haveria diferença significativa na resposta se a troca fosse 1 realizada entre parâmetros de um mesmo tipo objeto. Embora JAVA faça a passagem de parâmetros dos tipos objeto (não primitivos) por cópia de referência, os efeitos da troca continuariam sendo válidos apenas internamente a função, não se reflitindo nos parâmetros reais. 3. Um TAD (tipo abstrato de dados) é definido pelo comportamento uniforme de um conjunto de valores. Embora a linguagem C não suporte a implementação do conceito de TADs, o programador pode simular o seu uso. Explique como isto pode ser feito. Descreva os problemas com essa aproximação. Para simular o uso de TADs em C, é necessário definir um tipo de dados simples e um conjunto de operações (subprogramas) que se aplicam sobre valores desse tipo, isto é, subprogramas que têm parâmetros desse tipo. Normalmente, a implementação das operações do TAD e quaisquer outras entidades de computação necessárias para a implementação dessas operações são definidas num arquivo de implementação (.c). No arquivo de interface (.h), o tipo da estrutura de dados é definido e os protótipos dos subprogramas correspondentes às operações do TAD são declarados, para que sejam disponibilizados para os programadores usuários. Assim, o programador usuário não necessita mais implementar o código das operações do TAD, o que torna o código mais legível e redigível, além do código usuário não precisar ser alterado caso seja necessário realizar uma alteração na implementação do tipo ou nas suas operações (desde que os cabeçalhos das operações não sejam modificados). Os problemas com essa aproximação são que, além dela não promover o encapsulamento de dados e operações em uma única unidade sintática, ela não impede o uso indisciplinado do TAD. A operação de inicialização do TAD, por exemplo, deve ser chamada explicitamente pelo programador. Caso o programador usuário esqueça ou não chame essa operação antes de qualquer outra, o uso correto do TAD ficará comprometido. Além disso, é importante observar que ele pode realizar operações adicionais sobre o TAD além das especificadas pelos subprogramas. Por exemplo, o programador pode acessar diretamente a estrutura interna do TAD, quebrando o ocultamento de informações. 4. Considere uma função em JAVA recebendo um objeto como único parâmetro e simplesmente realizando a atribuição de null ao seu parâmetro formal. Qual o efeito dessa atribuição no parâmetro real? Justifique. Essa atribuição não produz qualquer efeito sobre o parâmetro real correspondente, só produzindo efeitos internos, uma vez que, em JAVA, as atribuições de valores completos do tipo não-primitivo ao parâmetro formal não produzem efeito no parâmetro real, sendo a passagem para tipos não-primitivos considerada, nesse caso, unidirecional de entrada. 5. JAVA não permite a criação de funções com lista de parâmetros variável, isto é, funções nas quais o número e o tipo dos parâmetros possam variar, tal como a função printf de C. Como JAVA faz para possibilitar a criação da função System.out.println com funcionalidade similar à função printf de C? Como o problema da falta de lista de parâmetros variável pode ser contornado de maneira geral pelo programador JAVA em situações nas quais esse tipo de característica 2 pode ser útil? Compare essa abordagem geral de JAVA com a adotada por C e C++ em termos de redigibilidade e legibilidade. A função System.out.println de Java recebe sempre uma string como argumento. Ela funciona de forma similar a printf por conta do uso combinado da operação de concatenação (+) de strings e da realização de conversão implícita de tipos antes da operação de concatenação. Tipos primitivos e objetos são convertidos implicitamente para strings antes da realização da concatenação (quando o outro argumento é uma string). Por exemplo, na chamada de System.out.println seguinte int i =3; System.out.println (“teste: “ + i + “: “+ new Float (4.3) + “: “+ true); o valor 3 de i é convertido na string “3” antes de ser concatenado à string
Compartilhar