Aula 2

1  Resumo da matéria

A inclusão no exemplo abaixo da biblioteca cassert destina-se a usar a função void assert(bool) que se encontra definida nesta biblioteca.

Esta função verifica se a condição passada como argumento é verdadeira e, caso não seja, termina o programa com uma mensagem de erro adequada.

Esta função é normalmente usada para verificar se as PC de determinada função ou procedimento se verificam.

1.1  Classe ListaInt (declaração)

Esta classe tem um iterador próprio (uma classe embutida, isto é, definida dentro doutra) que referencia um qualquer elemento (ou item) da lista.  Considera-se que existem elementos fictícios imediatamente antes do primeiro (índice -1) e após o último (índice comprimento()).  Quando se acede ao item indicado por um iterador, deve ser sempre verificado se o índice do iterador é válido na lista em causa (use assert()).

A necessidade de definir iteradores para elementos fictícios nas listas prende-se com o processo usado para as percorrer.  Como já deve ter verificado, quando se itera ao longo de uma matriz com n posições, o índice usado para iterar termina com o valor n (inválido como índice), ou com o valor -1 (inválido como índice) se se iterar em sentido inverso.  Por isso faz sentido que estes elementos fictícios sejam referências válidas para um iterador.

#include <cassert>
#include <iostream>

using namespace std;

// Uma classe para representar listas de elementos do tipo Item (actualmente int).

class ListaInt {
public:
    // Cria-se um sinónimo para simplificar a tarefa de criar listas para
    // elementos de outros tipos:
    typedef int Item;

    // Declaração de uma classe embutida que serve para percorrer e manipular listas.
    class Iterador;

    // A classe de iteração tem acesso às listas:
    friend class Iterador;

    // Construtor da classe, cria uma lista vazia:
    ListaInt();
    // Acrescenta item no início da lista:
    void poe_inicio(const Item& item);
    // Retira o primeiro item da lista:
    void tira_primeiro();
    // Acrescenta item no fim da lista:
    void poe_fim(const Item& item);
    // Retira o último item da lista:
    void tira_ultimo();

    // Insere item imediatamente antes da posição indicada pelo iterador
    // i, fazendo com que o iterador continue a referenciar o mesmo item:
    void insere(Iterador& i, const Item& item);
    // Remove o item indicado pelo iterador i, ficando o iterador a
    // referenciar o item logo após o item removido:
    void remove(const Iterador& i);
    // Devolve o comprimento da lista:
    int comprimento() const;

    // Devolve um iterador referenciando o primeiro item da lista:
    Iterador primeiro();
    // Devolve um iterador referenciando o último item da lista:
    Iterador ultimo();
    // Devolve um iterador referenciando o elemento fictício depois do último
    // elemento da lista:
    Iterador fim();
    // Devolve um iterador referenciando o elemento fictício antes do primeiro
    // elemento da lista:
    Iterador inicio();

private:
    enum{limite = 100};
    // Dever-se-ia usar:
    //    static const int limite = 100;
    // mas o Visual C++ não o permite, pois não implementa a norma do C++
    // como deveria.

    // Matriz que guarda os itens da lista:
    Item itens[limite];

    // Contador do número de itens na lista:
    int quantos;
};

// Uma classe para representar iteradores para itens de listas ListaInt:
class ListaInt::Iterador {
    // Referência para a lista a que o iterador está associado:
    ListaInt& lista;
    // Índice do elemento referenciado:
    int indice;

public:
    // Construtor da classe.  Associa o iterador com a lista lista e põe-no
    // a referenciar o seu primeiro item:
    explicit Iterador(ListaInt& lista);

    // Incrementação prefixa de iteradores (avança para o próximo item):
    Iterador& operator ++ ();
    // Decrementação prefixa de iteradores (recua para o item anterior):
    Iterador& operator -- ();

    // Devolve o item referenciado pelo iterador:
    Item& item();

    // O operador de comparação entre itens tem acesso aos membros privados da
    // classe Iterador:
    friend bool operator == (const Iterador& i1,
                             const Iterador& i2);

    // A classe ListaInt tem acesso a todos os membros da classe Iterador:
    friend class ListaInt;
};

// Definição das funções e procedimentos membro da classe ListaInt:
ListaInt::ListaInt() : quantos(0) {
    // Usa-se uma lista de inicializadores.  Neste caso basta dizer que a
    // lista está vazia, i.e., não tem qualquer item.
}

void ListaInt::poe_inicio(const Item& item) {

    // Só se pode inserir se ainda houver espaço:
    assert(comprimento() != limite);

    // Inserir no início implica rearranjar todos os itens já na lista:
    for(int i = comprimento(); i != 0; i--)
        itens[i] = itens[i - 1];

    // Agora já há espaço:
    itens[0] = item;

    // E convém incrementar o contador de elementos...
    quantos++;
}

void ListaInt::tira_primeiro() {
    // ...a completar na Aula 2.
}

void ListaInt::poe_fim(const Item& item) {
    // ...a completar na Aula 2.
}

void ListaInt::tira_ultimo() {
    assert(comprimento() != 0);

    quantos--;
}

void ListaInt::insere(Iterador& iterador, const Item& item) {
    // Só se pode inserir se ainda houver espaço:
    assert(comprimento() != limite);

    // Não se pode inserir se o iterador referir o elemento antes do primeiro:
    assert(iterador.indice != -1);

    // Há que rearranjar todos os itens a partir do referenciado pelo iterador:
    for(int i = comprimento(); i != iterador.indice; i--)
        itens[i] = itens[i - 1];

    // Agora já há espaço (não esquecer de revalidar o iterador!):
    itens[iterador.indice++] = item;

    // Mais um...
    quantos++;
}

void ListaInt::remove(const Iterador& iterador) {
    // ...a completar na Aula 2.
}

int ListaInt::comprimento() const {
    return quantos;
}

ListaInt::Iterador ListaInt::primeiro() {
    // Cria-se um iterador para esta lista, que referencia logo o primeiro
    // item da lista (ver construtor de ListaInt::Iterador):
    Iterador iterador(*this);

    // Devolve-se o iterador criado:
    return iterador;
}

ListaInt::Iterador ListaInt::ultimo() {
    // Cria-se um iterador para esta lista:
    Iterador iterador(*this);

    // O iterador deve referenciar o último item da lista:
    iterador.indice = comprimento() - 1;

    return iterador;
}

ListaInt::Iterador ListaInt::inicio() {
    Iterador iterador(*this);

    // O iterador deve referenciar o item fictício antes do primeiro item da lista:
    iterador.indice = -1;

    return iterador;
}

ListaInt::Iterador ListaInt::fim() {
    Iterador iterador(*this);

    // O iterador deve referenciar o item fictício após o último item da lista:
    iterador.indice = comprimento();
 

    return iterador;
}

// Definição das funções e procedimentos membro da classe ListaInt::Iterador:
ListaInt::Iterador::Iterador(ListaInt& l)  : lista(l), indice(0) {
}

ListaInt::Iterador& ListaInt::Iterador::operator ++ () {
    // ...a completar na Aula 2.

    return *this;
}

// Operador de incrementação sufixa.  Não é membro da classe (porquê?).
ListaInt::Iterador operator ++ (ListaInt::Iterador& iterador, int) {
    ListaInt::Iterador resultado = iterador;
    ++iterador;
    return resultado;
}

ListaInt::Iterador& ListaInt::Iterador::operator -- () {
    // Decrementar para aquém do elemento fictício inicial é um erro:
    assert(indice != -1);

    indice--;

    return *this;
}

// Também não é membro da classe:
ListaInt::Iterador operator -- (ListaInt::Iterador& iterador, int) {
    ListaInt::Iterador resultado = iterador;
    --iterador;
    return resultado;
}

// Comparam-se os índices.  Esta implementação não é perfeita.  Em rigor
// dever-se-iam comparar também as listas respectivas (pense nisto quando
// tiver aprendido ponteiros).  Nem a igualdade nem a desigualdade são funções
// membro.  Deveriam sê-lo?  Argumente a favor e contra.
bool operator == (const ListaInt::Iterador& i1, const ListaInt::Iterador& i2) {
    return i1.indice == i2.indice;
}

bool operator != (const ListaInt::Iterador& i1, const ListaInt::Iterador& i2) {
    return !(i1 == i2);
}

ListaInt::Item& ListaInt::Iterador::item() {
    // O item só existe se o iterador não se referir a um iterador fictício:
    assert(0 <= indice && indice < lista.comprimento());

    return lista.itens[indice];
}

// Definição do operador << para escrita de listas num canal:
// ListaInt& deveria ser const ListaInt, mas isso implica a definição de um
// Iterador para listas constantes
ostream& operator << (ostream& out, ListaInt& l) {
    out << '(';

    for(ListaInt::Iterador i = l.primeiro(); i != l.fim(); i++) {
        out << i.item();
        if(i != l.ultimo())
            out << ", ";
    }
    return out << ')';
}

// E finalmente um programa de teste:
int main()
{
    ListaInt l;

    l.poe_fim(1);
    l.poe_fim(2);
    l.poe_fim(3);
    l.poe_fim(4);

    for(ListaInt::Iterador i = l.primeiro(); i != l.fim(); ++i)
        cout << i.item() << ' ';
    cout << endl;

    for(ListaInt::Iterador j = l.ultimo(); j != l.inicio(); --j)
        cout << j.item() << ' ';
    cout << endl;

    cout << l << endl;

    ListaInt::Iterador k = l.primeiro();
    ++k;

    l.insere(k, 10);

    cout << l << endl;

    l.remove(k);

    cout << l << endl;

    l.tira_primeiro();
    cout << l << endl;

    l.tira_ultimo();
    cout << l << endl;

    l.tira_primeiro();
    cout << l << endl;

    l.tira_ultimo();
    cout << l << endl;

    l.tira_ultimo();
    cout << l << endl;
}

1.2  Leitura recomendada :

Recomenda-se a leitura do Capítulo 3 de [1].

2  Exercícios

1.  Complete o código acima e construa um módulo com a implementação da classe ListaInt dada na aula teórica e da classe de iteradores correspondente.

Use o main() do código acima para criar um programa de teste para esta classe (este programa deve encontrar-se num ficheiro diferente dos usados para definir a lista).

2.a)  Faça uma classe Aluno para representar as notas de um aluno a uma cadeira. Pretende-se registar o número e nome do aluno e as notas.  As notas reflectem o sistema de avaliação que se segue: ou se faz um trabalho (50%) e uma frequência (50%) ou, alternativamente, um exame.  O aluno tem a nota final apurada quando tiver nota no trabalho e na frequência ou nota de exame.  Se tiver as duas, a nota final é a melhor das duas.

Faça um programa de teste em que se dá o número e o nome de um aluno.  Depois o programa pergunta se quer dar a nota de trabalho.  Se o utilizador disser que sim o programa lê esta nota.  E da mesma forma para as outras duas notas.  No fim o programa indica que o aluno já tem nota final e qual ela é.

2.b)  Refaça a lista de 1. para conter elementos do tipo Aluno.  Assinale no .h e .cpp todas as modificações que teve que fazer.  Faça um programa de teste semelhante ao de 1.

3.  Considere a ListaInt feita em 1.  Pretende-se fazer um iterador que permita percorrer a lista de forma ordenada.  Isto é, começa por se obter um iterador para o menor valor que está na lista (ou o primeiro deles se houver mais que um igual).  Depois, ao incrementar o iterador, deve-se obter o iterador para o elemento seguinte por ordem crescente.  Para resolver este execício siga os passos abaixo:

3.a)  Faça uma classe embutida iteradorMenor que permita percorrer todos os elementos com o menor valor da lista.

A classe ListaInt deve ser equipada de uma função membro IteradorMenor menor() que devolva um iterador para o primeiro dos elementos com o menor valor na lista.  Se a lista estiver vazia o iterador deve-se referir ao elemento fictício final (depois do último).  A procura deste elemento é trivial, dados os algoritmos desenvolvidos no primeiro semestre.

A incrementação de um IteradorMenor faz o seguinte: vai desde a posição dada pelo iterador antes da incrementação até ao fim da lista à procura de um valor igual.  Se o encontrou, o novo valor do iterador deve-se referir a esse elemento.  Caso contrário o iterador deve-se referir ao elemento fictício final.

Faça um programa de teste que lê uma série de números para um lista e escreve os menores.

3.b)  Transforme a classe IteradorMenor na classe (embutida) IteradorOrdenado.  A inicialização deste iterador faz-se como a do IteradorMenor.  O avanço também, excepto que, se não encontrar um elemento de valor igual, deve colocar o iterador a referenciar o primeiro elemento da lista com valor imediatamente acima.  Se não existir, aí sim, deve referenciar o elemento fictício final.

Faça um programa de teste que lê uma série de números para uma lista e devolve-os de forma ordenada.  O ciclo de escrita dos valores deve ser:

ListaInt l;
... // leitura de valores.
// Escrita ordenada:
for(ListaInt::IteradorOrdenado i = l.menor(); i != l.fim(); ++i)
    cout << l.item() << endl;

3  Exercícios propostos

1.a)  Refaça a classe embutida IteradorOrdenado de 3., agora para o tipo Aluno.  Considere que os alunos são ordenados pelo número.

Faça um programa de teste que leia uma série de 10 alunos (número e nome)e os apresente ordenados por número.

1.b)  Modifique para ordenar pelo nome.

4  Referências

[1]  Michael Main e Walter Savitch, "Data Structures and Other Objects Using C++", Addison-Wesley, Reading Massachusetts, 1997.