Resolução do Exame de 1 ª época

Programação Orientada para Objectos

IGE e ETI

2º semestre de 2000/2001

ISCTE


A azul (e indentadas uma vez) as respostas às questões.

Questão 1

Assinale com V (Verdadeiro) as expressões que estão correctas e com F (Falso) as que estão incorrectas.

Deve preencher todos os espaços indicados por um sublinhado (      ) com V ou F.  Qualquer espaço não preenchido será considerado como uma resposta errada.

As alíneas podem ter zero ou mais respostas correctas.  Cada resposta correctamente assinalada vale 0,5 valores.

Nas alíneas em que apenas uma resposta está correcta (se existirem estão assinaladas no texto), responder com mais ou menos do que um V anula a cotação.  A resposta correcta corresponde à cotação completa.  Qualquer outra resposta corresponde a zero valores.

Considere que todas as operações têm métodos associados mesmo quando não são  explicitamente indicados.

Em todos os casos em que não é explicitamente referida a localização de uma instrução, considere que esta é dada na função main() do programa seguinte:

#include <iostream>

using namespace std;

class Base {

  public:
    Base(int a);
    virtual ~Base() {}

    string nomeDoTipo() const;

    virtual void método();

    int a_;

};

string Base::nomeDoTipo() const 
{
    return "Base"; 
}


void Base::método() 
{
    cout << nomeDoTipo() << endl;
}


class Derivada: public Base {

  public:
    Derivada(int d);

    string nomeDoTipo() const;

    virtual void método();

  private:

    int d_;
};

string Derivada::nomeDoTipo() const 
{
    return "Derivada";
}


void Derivada::método()
{
    cout << nomeDoTipo() << endl;
}

int main() 
{

    ...
} 

1.1  Quais das seguintes instruções estão correctas?

  F   Base base;

A classe Base não tem construtor por omissão.  O único construtor existente exige um argumento inteiro.

  F   Derivada.a_ = 2;
  F  
Derivada.d_ = 2;

Ambas as expressões estão sintacticamente erradas.  Os atributos a_ e d_ são-no de instância, pelo que não faz sentido aceder a eles através do nome da classe: o acesso teria de ser feito através de uma instância da classe.  Note-se que, mesmo que os atributos fossem de classe (i.e., partilhados entre todas as instâncias da classe, o que se consegue com a palavra chave static) as expressões estariam erradas, pois o acesso aos membros de uma classe através do nome de uma classe faz uso do operador ::, e não do operador ..

[Como aparte, na linguagem Java (e quejandas) o acesso a membros através do nome de uma classe faz-se usando o operador ..]

[cotação: 1,5]

1.2  Considere a seguinte a função main():

int main()
{

    Base* base = new Derivada(3);
    Derivada* derivada = new Derivada(4);

    base->método();
    derivada->método();

    delete base;
    delete derivada;
}

qual o resultado produzido pelo programa (apenas uma das respostas está correcta)?

  F   Base
    Derivada

  F   Base
    Base

  V   Derivada
    Derivada

No código acima são construídos dois objectos, ambos da classe Derivada.  Esses objectos são apontados por ponteiros de classes diferentes: Base e Derivada.  Em seguida invoca-se a operação método() através de ambos os ponteiros.  Esta operação é virtual, pelo que o método que é de facto executado é dado pela classe dos objectos, e não dos ponteiros.  Ou seja, em ambos os casos é executado o método método() da classe Derivada.  Esse método insere no canal de saída o resultado da invocação de uma operação chamada nomeDoTipo().  Essa operação é não-virtual, pelo que o método invocado é dado pela classe do ponteiro ou referência através do qual a operação é invocada.  Mas neste caso não parece existir nenhum ponteiro ou referência!  Não parece, mas existe.  Tudo funciona como se se invocasse a operação através do ponteiro this:

cout << this->nomeDoTipo() << endl;

Acontece que o ponteiro this é sempre um ponteiro para a classe onde o código surge, neste caso Derivada.  Assim, em ambos os casos será invocado o método dessa classe.

[Note-se que isso acontecia neste caso mesmo que o operação nomeDoTipo() fosse virtual.  Note-se também que o tipo do ponteiro original, na função main(), não tem qualquer relevância para a selecção do método a executar.]

Em bom rigor, não se pode dizer que haja uma operação nomeDoTipo() para a qual sejam fornecidos dois métodos.  Isso só aconteceria se a operação fosse virtual (na classe base).  Não sendo assim, existem de facto duas operações nomeDoTipo() com idênticas assinaturas, uma na classe base e outra na classe derivada.  Para ambas é fornecido o respectivo método (a implementação da operação).  Ou seja, em vez de haver sobrecarga (fornecimento de um método específico para uma dada operação), o que existe é uma ocultação, no sentido em que a redefinição da operação na classe derivada torna invisível a operação da classe base.

É muito importante perceber-se que é muito má ideia fazer ocultações numa hierarquia de classes (excepto se a herança for privada).  Sempre que uma operação for declarada como não-virtual numa classe, não deve ser redefinida em classes que dela derivem publicamente, directa ou indirectamente.  Ou seja, o código apresentado é um mau exemplo e se ocorrer na prática denota provavelmente um erro conceptual do programador.

[cotação: 0,5]

1.3  Quais das seguintes afirmações estão correctas?

  F   Os objectos da  classe Base herdam o comportamento definido na classe Derivada.

Nada é herdado pela classe Base a partir da classe Derivada.  Quando muito seria o contrário.

  F   A classe Base é abstracta porque tem operações abstractas.

A verdade é que não tem.  Operações abstractas são operações puramente virtuais, cuja declaração em C++ termina em = 0.

[cotação: 1]

1.4  Dado o seguinte código:

double a = 3.0;
int* b = 0;
float c = 3.14;
int const* d = new int[4];
int* const e = new int[5];

Quais das seguintes instruções estão correctas?

  F    int* resultado = a < 3.5 ? a : 3.5;

Não se pode inicializar um ponteiro com um inteiro: os tipos são incompatíveis.

  F    b = &c;

Não se pode inicializar um ponteiro para um int  com o endereço de um float: os tipos são incompatíveis.

  V    ++d;

O ponteiro não é constante, pelo que se pode alterar.  O que é constante é a variável apontada pelo ponteiro.

  F    ++e;

Caso oposto.  O ponteiro é constante, embora a variável apontada não o seja.

[cotação: 2]

Questão 2

Uma fábrica de carpintaria dedica-se à construção de ilhargas, prateleiras e outros componentes procurados por quem pretende construir estantes.

A determinada altura, a administração da fábrica decidiu vender os seus produtos directamente ao público.

O responsável pela venda directa achou necessária uma  aplicação que apoiasse o seu sector de vendas e que fosse responsável pela manutenção do catálogo de componentes a vender: Gestor de Catálogo.  Assim,  através desta aplicação é possível consultar informação  sobre os componentes, podendo encontrar-se resposta sobre dimensões e preços unitários.

Cada componente a vender caracteriza-se por:

Existem vários tipos de componentes dos quais se destacam as ilhargas e as prateleiras.  É objectivo da fábrica aumentar a diversidade de tipos de componentes que fabrica e vende directamente ao público.

As ilhargas são as partes laterais das estantes e podem suportar prateleiras e outros componentes.  Existe uma distância mínima, descrita em centímetros, entre os componentes suportados pelas ilhargas.  Esta medida serve apenas para informar o cliente, não limita a venda de prateleiras.  A fabrica produz ilhargas de dimensões variadas.

As prateleiras podem apoiar-se em ilhargas ou directamente na parede (todas as prateleiras são polivalentes) mas, em ambos os casos, é necessário saber o número de suportes precisos para o apoio.

Para a concepção da aplicação de gestão do catálogo foi pensado usar as classes Componente, Ilharga e Prateleira.  A classe Componente representa o que é comum a todas os componentes, a classe Ilharga e a classe Prateleira representam os componentes ilharga e prateleira, respectivamente.

Existem outros componentes como gavetas, armários, etc., mas para a resolução deste grupo de perguntas considere apenas os componentes prateleiras e ilhargas. Contudo, decida sempre por soluções que permitam facilmente estender o Gestor de Catálogo a outros componentes.

A definição de qualquer das classes deve possuir operações para:

2.1  Defina as classes Componente, Ilharga e Prateleira.  Não defina nenhum método.

class Componente {
  public:
    Componente(int const referência, string const& descrição);
    Componente(istream& entrada);
    virtual ~Componente() = 0;

    int referência() const;

   
string const& descrição() const;
    virtual int altura() const = 0;
    virtual int largura() const = 0;
    virtual int profundidade() const = 0;
    virtual double preço() const = 0;

   
virtual void guardaEm(ostream& saída) const;
    virtual void mostraEm(ostream& saída) const;

    virtual string nomeDoTipo() const = 0;

   
virtual void carregaDe(istream& entrada);

  private:
    int referência_;
    string descricao_;

    bool cumpreInvariante() const;
};

class Ilharga : public Componente {
  public:
    Ilharga(int const referência, string const& descrição,
            int const altura, int const largura, int const profundidade,
            double const preço,
            int distância_mínima_entre_componentes);
    Ilharga(istream& entrada);
    virtual ~Ilharga() {}

    int distânciaMínimaEntreComponentes() const;

   
virtual int altura() const;
    virtual int largura() const;
    virtual int profundidade() const;
    virtual double preço() const;

    virtual void guardaEm(ostream& saída) const;
    virtual void mostraEm(ostream& saída) const;

    virtual string nomeDoTipo() const;

    virtual void carregaDe(istream& entrada);

  private:
   
int altura_;
    int largura_;
    int profundidade_;
    double preco_;
    int distância_mínima_entre_componentes;

    bool cumpreInvariante() const;
};

class Prateleira : public Componente {
  public:
    Prateleira(int const referência, string const& descrição,
               int const altura, int const largura, int const profundidade,
               double const preço, int número_de_suportes_necessários);
   
Prateleira(istream& entrada);
    virtual ~Prateleira() {}

    int númeroDeSuportesNecessários() const;

    virtual int altura() const;
    virtual int largura() const;
    virtual int profundidade() const;
    virtual double preço() const;

    virtual void guardaEm(ostream& saída) const;
    virtual void mostraEm(ostream& saída) const;

    virtual string nomeDoTipo() const;

    void carregaDe(istream& entrada);

private:
    int altura_;
    int largura_;
    int profundidade_;
    double preco_;
    int número_de_suportes_necessários;

    bool cumpreInvariante() const;
};

[cotação: 1,5]

2.2  Defina os métodos responsáveis pela escrita no canal  do conteúdo de um objecto pertencente à classe Ilharga.  O método deve escrever todos os dados que definem uma ilharga.  O formato é deixado ao seu critério.

virtual void Ilharga::guardaEm(ostream& saída) const
{
    assert(cumpreInvariante());
    assert(saída);

    Componente::guardaEm(saída);

    saída << altura() << endl
          << largura() << endl
          << profundidade() << endl
          <<
preco() << endl
          << distânciaMínimaEntreComponentes() << endl;

    if(not saída)
        throw /*
classe representando erro ao guardar, por definir. */();

}

[cotação: 1,5]

Após alguns meses de experiência do sistema de venda directa achou-se conveniente vender composições de componentes, ou seja, módulos, destinados a estantes.  Os módulos para estantes possuem um número indeterminado de ilhargas, prateleiras e outros componentes.

A resolução desta questão é uma simplificação da resolução realmente necessária.  Considere que é possível conceber um módulo de estantes em que alguns dos componentes são outros módulos de estantes, o que não é verdade.

O cálculo do preço de cada módulo é a soma dos preços afectados dos possíveis descontos.

A altura do módulo é a altura máxima das ilhargas (que podem ser de alturas diferentes).  O largura do módulo é a soma da largura das ilhargas e da largura máxima das componentes.  A profundidade é o máximo das profundidades entre as ilhargas e os componentes.

2.3 Defina a classe MóduloDeEstante, que representa o módulo de estante descrito anteriormente, mas não defina os seus métodos.  Guarde a lista de componentes que compõe um módulo usando a classe list<Componente const*>.

Note-se que os módulos não são possuidores dos componentes respectivos.  Limitam-se a agregar componentes que são parte do catálogo.  Por isso o destrutor não destrói os respectivos componentes!

class MóduloParaEstante : public Componente {
  public:
    MóduloParaEstante(int const referência, string const& descrição);
   
MóduloParaEstante(istream& entrada);
    virtual ~MóduloParaEstante() {}

    virtual int altura() const;
    virtual int largura() const;
    virtual int profundidade() const;
   
virtual double preço() const;

    virtual void guardaEm(ostream& saída) const;
    virtual void mostraEm(ostream& saída) const;

    virtual string nomeDoTipo() const;

    void adiciona(Componente const* const novo_componente);
    void removeComponenteComReferência(int referência);

    void carregaDe(istream& entrada);

  private:
    list<Componente const*> componentes;

    bool cumpreInvariante();
};

[cotação: 1,5]

2.4  Defina o método responsável pelo cálculo do preço de um módulo.

double MóduloParaEstante::preço() const
{
    assert(cumpreInvariante());

    double preço = 0.0;

    for(list<Componente const*>::const_iterator i = componentes.begin();
        i != componentes.end(); 
        ++i)
        preço += (*i)->preço();

    return preço;
}

[cotação: 1]

A aplicação Gestor de Catálogo  baseia-se na classe GestorDeCatálogo que disponibiliza as seguintes funcionalidades:

2.5  Defina a classe GestorDeCatálogo.  Não especifique o conteúdo de qualquer dos métodos.  Use a classe list<Componente*> para definir a lista de componentes mantida pela classe GestorDeCatálogo.

class GestorDeCatálogo {
  public:
    GestorDeCatálogo();
    GestorDeCatálogo(istream& entrada);
    ~GestorDeCatálogo();

   
bool contémComponenteComReferência(int const referência) const;
    double preçoDeComponenteComReferência(int const referência) const;

    void guardaEm(ostream& saída) const;
    void mostraEm(ostream& saída) const;

    void adiciona(Componente* const novo_ componente);
    void removeComponenteComReferência(int const referência);

    void carregaDe(istream& entrada);
    void carregaMaisComponentesDe(istream& entrada);

  private:
    list<Componente*> componentes;

    bool cumpreInvariante() const;
};

[cotação: 0,5]

2.6  Defina o método responsável pela adição de um componente à lista de componentes mantida pela classe GestorDeCatálogo.  Não se esqueça que este método deve manter a lista ordenada segundo o referido no enunciado, por ordem crescente ou decrescente (fica ao seu critério).

namespace {

    bool menor(Componente const& a, Componente const& b)
    {
        return a.referência() < b.referência() or
            (a.referência() == b.referência() and a.preço() < b.preço());
    }

}

void GestorDeCatálogo::adiciona(Componente* const novo_ componente)
{
    assert(cumpreInvariante());
    assert(componente != 0);

    list<Componente*>::iterator i = componentes.begin();
    while(i != componentes.end() and menor(**i, *componente))
        ++i;

   
componentes.insert(i, componente);

    assert(cumpreInvariante());
}

[cotação: 1,5]

2.7  Defina o destrutor de GestorDeCatálogo.

Admite-se que se usa uma política de gestão de variáveis dinâmicas "quem tem destrói".  Esta política implica que não haja dois detentores da mesma variável dinâmica.

GestorDeCatálogo::~GestorDeCatálogo() 
{
    assert(cumpreInvariante());

    for(list<Componente*>::const_iterator i = componentes.begin();
        i != componentes.end();
        ++i)
        delete *i;
}

[cotação: 0,5]

2.8  Defina o método da classe GestorDeCatálogo responsável pela inserção num canal das definições de componentes.

Os dados inseridos no canal devem obedecer ao seguinte formato:

número_de_componentes
tipo_do_primeiro_componente
...informação do primeiro componente
...
tipo_do_último_componente
...informação do último componente

void GestorDeCatálogo::guardaEm(ostream& saída) const
{
    assert(cumpreInvariante());
    assert(saída);

    saída << componentes.size() << endl;

    for(list<Componente*>::const_iterator i = componentes.begin();
        i != componentes.end();
        ++i) {
        if(not (saída << (*i)->nomeDoTipo() << endl))
            throw /* classe representando erro ao guardar, por definir. */();

        (*i)->guardaEm(saída);
    }

    if(not saída)
        throw /* classe representando erro ao guardar, por definir. */();
}

[cotação: 1]

2.9  Defina o método da classe GestorDeCatálogo responsável pela extracção de um canal das definições dos componentes.

Os dados extraídos do canal devem obedecer ao mesmo formato usado na alínea anterior.

O enunciado não é claro, pelo que se optou por implementar o método que acrescenta componentes.

void GestorDeCatálogo::carregaMaisComponentesDe(istream& entrada) 
{
    assert(cumpreInvariante());
    assert(entrada);

    int número_de_componentes;
    if(not (entrada >> número_de_componentes))
        throw /*
classe representando erro ao carregar, por definir. */();


    //
aqui é fundamental limpar o canal até ao fim de linha inclusive!

    while(número_de_componentes-- != 0) {
        string nome_do_tipo;

        if(not (entrada >> nome_do_tipo))
            throw /*
classe representando erro ao carregar, por definir. */();


        //
aqui é fundamental limpar o canal até ao fim de linha inclusive!

        if(tipo == "Ilharga")
            adiciona(new Ilharga(entrada));
        else if(tipo == "Prateleira")
            adiciona(new Prateleira(entrada));
        else if(tipo == "MóduloParaEstante")
            adiciona(new ModuloParaEstante(entrada));
        else
            throw /*
classe representando erro ao carregar, por definir. */();

    }

    if(not entrada)
        throw /* classe representando erro ao carregar, por definir. */();

    assert(cumpreInvariante());
}

Note-se que este método não resiste convenientemente ao lançamento de excepções durante a construção ou inserção dos componentes, podendo o seu trabalho ficar parcialmente realizado.  Torná-lo robusto face ao lançamento de excepções fica como exercício para o leitor...

[cotação: 1]

Questão 3

Considere a seguinte estrutura

struct Elo {
    typedef int Item;

    Item item;     //
o item propriamente dito.
    Elo* anterior; // ponteiro para o elo anterior na cadeia.
    Elo* seguinte; // ponteiro para o elo seguinte na cadeia.

    ///
Construtor de elos.  Simplifica a construção de novos elos.
    Elo(Elo* anterior = 0, Elo* seguinte = 0, Item const& item = Item());
};

usada para representar os elos de uma cadeia duplamente ligada (não-circular) com guardas.

3.1  Defina o procedimento void põe(Elo* elo_anterior, Elo* elo_seguinte, Elo::Item const& novo_item) que inserira um novo item numa cadeia duplamente ligada (não-circular) com guardas.  O elo do novo item é inserido entre os dois elos apontados por elo_anterior e elo_seguinte, que se supõem sucessivos.  Estes ponteiros não podem nunca ser nulos.

Faça desenhos da cadeia em várias situações!  Só assim resolverá com sucesso esta alínea.

void põe(Elo* const elo_anterior, Elo* const elo_seguinte, Elo::Item const& novo_item)
{

    assert(elo_anterior != 0 and elo_seguinte != 0 and
           elo_anterior->seguinte = elo_seguinte and
           elo_anterior= elo_seguinte->anterior);

    elo_anterior->seguinte = elo_seguinte->anterior =
        new Elo(elo_anterior, elo_seguinte, novo_item);
}

[cotação: 0,5]

3.2  Implemente uma função que indica se, numa cadeia duplamente ligada (não-circular) com guardas, um dado item existe (ou não).  A cadeia é representada por um ponteiro para o elo antes do primeiro (guarda inicial) e por um ponteiro para o elo depois do último (guarda final).

Faça desenhos da cadeia em várias situações!  Só assim resolverá com sucesso esta alínea.

bool contémItem(Elo const* const inicial, Elo const* const final,
                Elo::Item const& item) 
{
    assert(inicial != 0 and final != 0 and 
           /*
função que verifica se entre inicial e final há caminhos em ambas as
              direcções passando por ordem inversa pelos mesmos elos. */);

    Elo const* elo = inicial->seguinte; 
    while(elo != final and elo->item != item)
        elo = elo->seguinte;

   
return elo != final;
}

[cotação: 1]

Questão 4

Considere a seguinte definição (incompleta) de uma pilha de inteiros:

class PilhaInt {
  public:
    typedef int Item;

    ///
Construtor da classe.  O argumento é a capacidade da pilha.
    PilhaInt(int capacidade = capacidade_inicial);

    PilhaInt(PilhaInt const& p);
    ~PilhaInt();

        void põe(Item const& novo_ item);

        ...

    bool estáVazia() const;

    bool estáCheia() const;

    ///
Devolve o número de itens na pilha, i.e., a sua altura.
    int altura() const;

   
...

  private:
    void duplicaCapacidade() const;

    static int const capacidade_inicial = 32;

    mutable int capacidade;

    int altura_;
    //
Matriz dinâmica dos itens:
    mutable Item* itens;
};

inline void PilhaInt::põe(Item const& novo_ item) {

    if(altura == capacidade)
        //
Não há espaço, logo duplica-se a capacidade:

        duplicaCapacidade();

    //
Agora já há espaço, pode-se inserir normalmente:

    itens[altura++] = novo_item;
}

void PilhaInt::duplicaCapacidade()

{
    Item* novos_itens = new Item[2 * capacidade];

    //
Copia-se para a nova matriz os itens que estavam na matriz original:

    for(int i = 0; i != altura; ++i)
        novos_itens[i] = itens[i];
 
    capacidade *= 2;


    //
Destrói-se a matriz dos itens original:
    delete[] itens;


    //
A matriz dos itens passa a ser a nova matriz construída (já com os itens antigos):

    itens = novos_itens;
}

int main()
{

    ...
}

Explique as alterações na definição da classe PilhaInt necessárias para que as falhas por falta de memória redundem no lançamento de uma excepção da classe PilhaInt::MemóriaEsgotada e não através do lançamento da excepção bad_alloc (i.e., traduza o tipo de excepção lançado).  Exemplifique as alterações usando os métodos void PilhaInt::põe(Item const& item) e void PilhaInt::duplicaCapacidade().

Defina as classes e métodos que achar necessários.

Ver Resumo da Aula 6.

[cotação: 2]

Questão 5

Um problema comum em grandes projectos é a existência de entidades (classes, variáveis, funções ou procedimentos) com o mesmo nome, embora desenvolvidos por pessoas, equipas ou empresas diferentes.  Quando isto acontece diz-se que ocorreu uma colisão de nomes.

Indique a solução a adoptar para minimizar a probabilidade de ocorrência deste problema.  Exemplifique usando a definição da classe PilhaInt fornecida na Questão 4 e admitindo que essa classe faz parte de um pacote de contentores de informação fornecido pela empresa O Barato Sai Caro.

Basta incluir todas as definições do ficheiro de interface (.H) do módulo físico que define a classe PilhaInt dentro de um (ou dois) espaços nominativos:

namespace OBaratoSaiCaro {

    namespace Contentores {

       
... // Definições do ficheiro pilha_int.H.

    }

}

[cotação: 1,5]