ISCTE - IGE/ETI

Programação II /Programação Orientada por Objectos

Resolução da frequência

1998/1999, 2º semestre



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.

Em geral 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 (estão assinaladas no texto), responder com mais do que um V anula a cotação, a resposta correcta corresponde à cotação completa.  Qualquer outra resposta corresponde a zero valores.

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>

class A {
    int a;
  public:
    A(int aa) : a(aa) {}
    void g() {
        std::cout << "Execução de A::g(). ";
    }
};

class B : public A {
  public:
    int bx;
    B(int, int);  // Não há construtor por omissão.
    void g() {
        A::g();
        std::cout << "Execução de B::g(). ";
    }
};

inline B::B(int aa, int bb) ...

int main() {
    ...
}

Questão 1.1
Quais das seguintes sequências de instruções estão correctas? (Têm de estar correctas as duas instruções!)

 F  B* p = new B; p->bx = 1;

A primeira instrução está errada, pois a classe B não tem construtor por omissão.  Assim, deveriam ter sido passados argumentos ao construtor, como acontece na sequência de instruções seguinte:
 F  B b(1, 2); b->bx = 1;
A segunda instrução está errada.  O operador de selecção de membro -> só se pode aplicar se o operando esquerdo for um ponteiro.  Mas b é uma instância da classe B, e não um ponteiro para B.
 F  B* p; B->bx = 1;
Neste caso a primeira instrução define p como um ponteiro para B, embora não o inicialize.  A segunda instrução, no entanto, continua errada: o primeiro operando do operador de selecção de membro -> deveria ser um ponteiro.  Neste caso nem sequer é um valor: é um tipo, o que é um erro.
 F  B& b; b.bx = 1;
Aqui é a primeira instrução que está errada.  Qualquer referência tem de ser inicializada.  Logo, a referência b deveria ter sido inicializada com uma instância qualquer de B.
[2 valores]
Questão 1.2
Quais dos seguintes construtores da classe B inicializam correctamente as variáveis membro A::a e B::bx?

 F  B::B(int aa, int bb) : a(aa), bx(bb) {}

A variável membro a pertence à classe base A, pelo que não pode ser inicializada directamente na classe derivada (o problema não é a ser privada de A).
 V  B::B(int aa, int bb) : A(aa), bx(bb) {}
Aqui a inicialização de a faz-se indirectamente (e correctamente...) invocando o construtor da classe base A na lista de inicializadores do construtor da classe derivada B.
 F  B::B(int aa, int bb) : bx(bb) { A(aa); }
Neste caso não é feita uma invocação explícita do construtor de A na lista de inicializadores do construtor de B.  Assim, o compilador tenta invocar implicitamente o construtor por omissão de A.  Como este não existe, é um erro.  Mesmo que o construtor por omissão existisse, o corpo do construtor (entre {}) seria sempre absurdo (embora sem qualquer erro sintáctico): a instrução A(aa); tem como resultado a criação de uma nova instância temporária da classe A que é imediatamente descartada.  É uma instrução tão inútil quanto a instrução 1;.
 [1.5 valores]
Questão 1.3
 Dado o seguinte código:
B b(1, 2);
A* pa = &b;
pa->g();
o que apareceria no ecrã (só uma opção verdadeira)?

 V  Execução de A::g().
 F  Execução de A::g(). Execução de B::g().
 F  Execução de B::g().

A classe B, derivada de A, sobrepõe a sua própria especialização do procedimento g().  Mas este não é virtual na classe base, pelo que a invocação de g() através de um ponteiro para A não tem comportamento polimórfico, ou seja, a versão invocada depende do tipo do ponteiro e não do tipo do objecto apontado.
 [1.5 valores]
Questão 2
Assuma que está completamente definida a classe Tarefa:
class Tarefa {
  public:

    // Construtor (não há construtor por omissão):
    Tarefa(string descrição, string notas);

    // Destrutor virtual:
    virtual ~Tarefa() {}

    // Acesso à descrição e às notas:
    string descrição() const;
    string notas() const;

    // Devolve duração da tarefa (em dias):
    virtual int duração() const = 0;

    // Devolve custo total da tarefa (em escudos):
    virtual int custo() const = 0;

    // Devolve a mão de obra necessária para a execução da tarefa em cada dia (em operários):
    virtual int mãoDeObra() const = 0;

  private:
    string descrição_;
    string notas_;
};

Questão 2.1
Defina uma classe concreta (não abstracta) TarefaMecanizada, derivada da classe Tarefa, que não necessita de mão de obra nem de material.  Esta tarefa deverá guardar o custo por dia de funcionamento do equipamento associado à tarefa, o tipo de equipamento que necessita (e.g., "Soldadora Omron"), bem como a duração da tarefa.  O custo total deste tipo de tarefa é dado apenas pelo custo de funcionamento do equipamento durante os dias de execução da tarefa.

Não é necessário nesta alínea definir qualquer um dos métodos da classe TarefaMecanizada.

 [4 valores]

class TarefaMecanizada : public Tarefa {
  public:
    // Construtor:
    TarefaMecanizada(string descrição, string notas,
                     int duração, int custo_diário,
                     const string& tipo_de_equipamento);

    // Sobreposição de funções às funções puramente virtuais da classe base:
    int duração() const;
    int custo() const;
    int mãoDeObra() const;

    // Funções membro de acesso aos dados:
    int custoDiário() const;
    string tipoDeEquipamento() const;

  private:
    // Variáveis membro:
    int duração_;
    int custo_diário_;
    string tipo_de_equipamento_;
};

Questão 2.2
Defina o construtor da TarefaMecanizada com os argumentos necessários à inicialização da classe base Tarefa e das variáveis privadas da própria classe TarefaMecanizada.

 [3 valores]

TarefaMecanizada::TarefaMecanizada(string descrição, string notas,
                                   int duração, int custo_diário,
                                   const string& tipo_de_equipamento)
    : Tarefa(descrição, notas), duração_(duração),
      custo_diário_(custo_diário),
      tipo_de_equipamento_(tipo_de_equipamento) {
}
Questão 2.3
Defina o método int TarefaMecanizada::mãoDeObra() const.

 [3 valores]

int TarefaMecanizada::mãoDeObra() const {
    return 0;
}
Questão 3
Seja o seguinte modelo Pilha para pilhas de itens de tipo arbitrário:
template <typename I>
class Pilha {
  public:
    // Construtor da classe:
    Pilha();
    // Coloca cópia de item no topo da pilha:
    void põe(const I& item);
    // Retira o item que está no topo da pilha:
    void tira();
    // Devolve cópia do item que está no topo da pilha:
    I topo() const;
    // Devolve true sse a pilha estiver vazia:
    bool vazia() const;
    // Devolve o número de itens na pilha:
    int altura() const;
  private:
    ...
};
Admita que a definição acima se encontra num ficheiro pilha.h.

Escreva um pequeno programa, utilizando o modelo de pilha fornecido, que peça ao utilizador para introduzir um texto e verifique se os seus parênteses estão correctamente balanceados.  Preveja apenas parêntesis de dois tipos: curvos (()) e rectos ([]).  Admita que o texto termina no primeiro ponto final (.) encontrado.   O programa deve ignorar o texto em si: só os parânteses são importantes.  Por exemplo, dadas as seguintes expressões, o programa deve escrever os erros indicados a itálico:

) Aqui ( há ) ( [ gato ] ) .
Fecho sem correspondente abertura.
( Aqui ] também .
Fecho com tipo de parênteses errado.
( Oops .
Parênteses por fechar.
[ este ) ( ] ] ) é ( ) [ ( comprido ) ( ) ] ( (.
Fecho com tipo de parênteses errado.
Fecho com tipo de parênteses errado.
Fecho sem correspondente abertura.
Fecho sem correspondente abertura.
Parênteses por fechar.
Parênteses por fechar.
Utilize o seguinte esqueleto de programa (não precisa de o reproduzir, basta identificar os três troços de programa a escrever: A, B e C):
#include <iostream>

#include "pilha.h"

int main() {
    std::cout << "Introduza um texto terminado por um ponto final."
              << std::endl;

    ... // A preencher.  Troço A.

    char c;
    while(std::cin.get(c) && c != '.')
        if(c == '(' || c == ')' || c == '[' || c == ']') {
          ... // A preencher.  Troço B.
        }

    ... // A preencher.  Troço C.
}

 [3 valores]
#include <iostream>

#include "pilha.h"

int main() {
    std::cout << "Introduza um texto terminado por um ponto final."
              << std::endl;

    Pilha<char> abertos;

    char c;
    while(std::cin.get(c) && c != '.')
        if(c == '(' || c == ')' || c == '[' || c == ']') {
            if(c == '(' || c == '[')
                abertos.poe(c);
            else if(abertos.vazia())
                std::cout << "Fecho sem correspondente abertura."
                          << std::endl;
            else {
                if((c == ']' && abertos.topo() != '[') ||
                   (c == ')' && abertos.topo() != '('))
                    std::cout << "Fecho com tipo de parênteses errado."
                              << std::endl;
                abertos.tira();
            }
        }

    while(!abertos.vazia()) {
        std::cout << "Parênteses por fechar" << std::endl;
        abertos.tira();
    }
}

Questão 4
Explique sucintamente em que circunstâncias um método (função ou procedimento membro de instância) deve ser puramente virtual e quais as consequências da declaração de um método desse tipo para a respectiva classe.

 [2 valores]
 

Um método deve ser puramente virtual quando corresponder a uma operação que faz sentido para qualquer classe concreta derivada da classe em causa, mas que não seja possível definir para esta classe (classe base).  Isso acontece quando a classe em causa representa um conceito abstracto, como uma forma ou um género na taxonomia dos seres vivos.  quando um método é declarado como puramente virtual, a respectiva classe não é obrigada a defini-lo, embora o possa fazer.  Uma classe com algum método puramente virtual, ou que tenha herdado algum método puramente virtual ao qual não tenha sobreposto um método não puramente virtual, diz-se, do ponto de vista do C++, abstracta.  As classes abstractas em C++ não podem ser instanciadas, ou seja, não podem existir instâncias (objectos ou variáveis, dinâmicas ou não) desse tipo.