typedef
. Item
e vantagens na facilidade
de adaptar a classe.stack
.Atenção! Fazer acetatos a partir do código.
Hoje vamos falar um pouco mais sobre desenho de TAD.
Normalmente os TAD não surgem do nada. Surgem por serem úteis para resolver um determinado problema. Vamos então resolver um problema.
Suponhamos que queremos implementar uma calculadora simples. Esta
calculadora deve suportar as operações de adição, subtracção,
multiplicação, divisão e potenciação sobre valores de vírgula flutuante
(usaremos double
). Para simplificar vamos supor que os
parênteses são proibidos.
As expressões a calcular devem ser lidas do canal de entrada cin
,
ligado ao teclado, e os resultados devem ser escritos no canal de saída cout
,
ligado ao ecrã. A calculadora pode calcular várias expressões, pelo que
se arbitra que estas são separadas umas das outras pelo símbolo ;
(ponto-e-vírgula). A calculadora termina o cálculo de expressões quando
encontra o símbolo . (ponto).
Por exemplo, se a entrada for:
2 + 2 ;
2 * 2.5 .
a saída deve ser
4
5
Para percebermos melhor como resolver o problema, vamos tentar perceber como nós próprios efectuaríamos os cálculos caso tivéssemos acesso a um símbolo de cada vez. Por símbolo quero dizer ou valores numéricos ou os operadores permitidos.
Seja então a expressão
1 + 4 * 3 / 2 ^ 3 - 2 .
Qualquer um de nós calcula rapidamente o resultado que é...
Exacto: 0,5.
E se só virmos um símbolo de cada vez. Que fazer?
Discutir.
Concluir que o melhor é ir guardando os valores e os operadores! Mas onde?
O.k., vamos passo a passo.
Primeiro recebemos
1
Que fazer?
Discutir.
Depois recebemos
+
Que fazer?
Discutir. Deixar a dica que talvez seja boa ideia usar um TAD para representar os operadores.
Depois recebemos
4
Pode-se já calcular?
Discutir.
Não! Porquê? Porque a seguir pode vir outro operador com maior precedência!
E se agora fosse
+
?
Discutir.
Nesse caso podia-se logo calcular a primeira operação!
O mesmo se a expressão até aqui fosse:
1 * 4 +
!
Mas não é...
Temos de ler mais um símbolo, que é:
*
Calma lá. Quantos valores já lemos? E quantos operadores? E onde os guardamos?
Discutir, mesmo sem concluir.
Depois vem
3
Pouco ajuda...
E depois vem
/
E agora?
Discutir!
Qual é a regra? Podem-se calcular todos os operadores em espera que tenham precedência superior ou igual ao que acabámos de ler!
Mas por que ordem?
Discutir. Concluir que os cálculos prosseguem pela ordem inversa à da leitura!
Ou seja, podemos empilhar as operações!
E os valores?
Discutir.
Uma pilha também serve para os valores!
Exemplificar desde o início ao fim da expressão, desenhando a evolução das pilhas. Explicar o que acontece no . (ponto).
Então já temos uma ideia da forma de cálculo da nossa calculadora. Falta definirmos os TAD necessários.
Em primeiro lugar, vai-nos dar muito jeito um TAD para representar os operadores!
Apresentar acetato! Explicar cuidadosamente a classe. Explicar
o operator()()
. Explicar o operador <: um operador é
menor que outro se tiver menos precedência. Explicar membro de classe
cuidadosamente! Dar exemplos de invocação!
Em segundo lugar vai-nos ser útil uma classe para representar a calculadora!
Apresentar acetato. Explicar detalhadamente o seu funcionamento. Deixar claro que não se verificam nenhuns erros do utilizador! A intenção é simplificar a resolução. Fica como exercício... Fazer um traçado admitindo a mesma entrada.
Depois de eles se terem convencido da bondade da solução, avançar.
Disse-vos que ia falar do desenho de TAD e na volta apresentei as classes C++ correspondentes já prontas e acabadas... Batotice... Bom, não inteiramente. Faltam as pilhas!
Explicar que ainda não temos os tipos PilhaDeDouble
nem PilhaDeOperador
,
mas que já temos um programa escrito que faz uso da classe!
Esta é uma boa forma de sabermos que operações deve um TAD suportar: começar a resolver o problema assumindo que a classe existe!
Vamos começar pela classe PilhaDeDouble
. A classe PilhaDeOperador
será praticamente igual.
Que operações podemos realizar sobre uma pilha?
Discutir. Concluir:
Antes de avançarmos, no entanto, é conveniente, aliás, é fundamental, fazermos aquilo que se chama um teste de unidade. Um teste de unidade, ou teste de módulo, é um pequeno programa que nos permite testar todas as operações de um TAD, de modo a ficarmos seguros do seu bom funcionamento.
.....
#ifdef TESTE
int main()
{
PilhaDeDouble pilha;
assert(pilha.estaVazia());
assert(pilha.altura() == 0);
for(int i = 0; i != 10; ++i) {
assert(pilha.altura() == i);
pilha.poe(i);
assert(pilha.itemNoTopo() == i);
assert(not pilha.estaVazia());
assert(pilha.altura() == i + 1);
}
assert(pilha.itemNoTopo() == 9);
assert(not pilha.estaVazia());
assert(pilha.altura() == 10);
for(int i = 9; i != -1; --i) {
assert(pilha.itemNoTopo() == i);
assert(not pilha.estaVazia());
assert(pilha.altura() == i + 1);
pilha.tiraItem();
assert(pilha.altura() == i);
}
assert(pilha.estaVazia());
assert(pilha.altura() == 0);
}
#endif
Explicar bem as directivas de pré-processador, embora sem entrar em
pormenores. Dizer que a função main()
só chega a ser compilada se o
programa for compilado com um comando da forma:
c++ -DTESTE
...
Agora é só ir definindo a interface da classe C++, incluindo documentação, mas deixando a implementação de lado.
Discutir efeito de cada operação e circunstâncias em que falha.
/**
Representa pilhas dedouble
.@invariant Dizer que não se conhece por ser questão de implementação.
*/
class PilhaDeDouble {
public:
typedef double Item;
/**
Constrói pilha vazia.@pre V.
@post
estaVazia
().*/
PilhaDeDouble();
/**
Devolve o item que está no topo da pilha.@pre
¬estaVazia()
.@post
itemNoTopo
idêntico ao item no topo de*this
.*/
Item const& itemNoTopo() const;
/**
Indica se a pilha está vazia.@pre V.
@post estaVazia =
*this
está vazia.*/
bool estaVazia() const;
/**
Devolve altura da pilha.@pre V.
@post
altura
= altura de*this
.*/
int altura() const;
/**
Põe um novo item na pilha (no topo).@pre V.
@post
*this
contém um item adicional no topo igual anovo_item
.*/
void poe(Item const& novo_item);
/**
Tira o item que está no topo da pilha.@pre ¬
estaVazia
().@post
*this
contém os itens originais menos o do topo.*/
void tiraItem();
private:
...
/**
Indica se a CIC se verifica.@pre V.
@post
cumpreInvariante
= ... .*/
bool cumpreInvariante() const;
};...
Discutir cuidadosamente o typedef
e sua utilidade!
Com Item
é fácil alterar o tipo dos itens!
O que fazer para definir a classe PilhaDeOperador
?
Discutir.
O que é que falta? A implementação! É o menos importante! Façam-na vocês: fica como exercício.
Conclusões:
O teste de unidade e a interface da classe podem ser desenvolvidos em paralelo, incrementalmente. Mas a implementação deve ser deixada para o fim!
Falar de modularização: departamento de marketing + designers definem a interface (o que faz, como se opera, aspecto exterior), departamento de engenharia implementa mecanismo.
Programação baseada em objectos: primeiro pensa-se que classes existem (substantivos no vocabulário do problema). Depois pensa-se nos algoritmos. E não o contrário: programação procedimental, em que primeiro se pensa nos algoritmos e só depois nos dados. Por vezes misturam-se os dois tipos de abordagem.
Se houver tempo discutir um pouco a implementação.
Dizer que as pilhas... Já existem... #include <stack>
...
push()
, pop()
, size()
, top()
,
empty()
.
Referir que no segundo semestre vamos aprender a escrever o código da pilha uma vez apenas de modo a que seja válido com qualquer tipo: classes genéricas e programação genérica.