ISCTE - IGE/ETI

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

Resolução da frequência tipo

1998/1999, 2º semestre


Questão 1
Assinale com V (Verdadeiro) as respostas 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.

Qualquer alínea pode ter zero ou mais respostas correctas. Cada resposta correctamente assinalada vale 0,5 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:

class X {
  public:
    ...
};

class A {
    int a;
    X* px;
  public:
    A();
    ~A();
    virtual void g() const {
        cout << "Execução de A::g(). ";
    }
    ...
};

class B: public A {
  public:
    B();
    void g() const {
        cout << "Execução de B::g(). ";
    }
    ...
};

int main() {
    ...
}

Questão 1.1
Dadas as declarações:
int m[10];
int* p;
Quais das seguintes sequências de instruções estão correctas quer do ponto de vista sintáctico quer do ponto de vista lógico?
 F  p = m; delete p;
A atribuição p = m é válida, pois a utilização do nome duma matriz numa expressão é equivalente à utilização dum ponteiro para o seu primeiro elemento.  Assim, p fica com o endereço do primeiro elemento de m.  Mas a instrução delete p; está errada, pois p não contém o endereço duma variável dinâmica, mas sim de uma matriz não dinâmica, e apenas se pode destruir explicitamente as variáveis ou matrizes dinâmicas (e estas com o operador delete[]).
 F  delete m;
Neste caso o erro é mais evidente: m não é uma variável dinâmica e por isso não pode ser destruida explicitamente.  As variáveis não dinâmicas são criadas e destruídas em instantes bem determinados.  Por exemplo, uma variável local automática é criada quando a sua instrução de definição é executada e destruída quando o bloco de instruções (tipicamente entre {}) em que foi definida chega ao fim.
 F  p = new int[10]; delete p;
Neste caso está-se a tentar destruir uma matriz dinâmica através do operador delete, quando as matrizes dinâmicas têm de ser destruídas através do operador delete[].
Questão 1.2
Sendo o construtor da classe A definido por:
A::A() : a(0), px(new X) {
}
quais os destrutores adequados para esta classe?
 F  A::~A() { delete a; delete px; }
A variável membro a não é dinâmica, não podendo portanto ser destruída explicitamente.
 V  ~A::~A() { delete px; }
É boa política destruir (devolver à memória livre) as variáveis dinâmicas depois de as utilizar.
 F  ~A::~A() { px = new X; delete px;}
Não faz sentido criar um variável dinâmica para a destruir logo de seguida.  Além disso a variável dinâmica criada no construtor fica perdida, i.e., há uma fuga de memória.
 F  ~A::~A() { delete *px; }
É um erro comum tentar destruir "o conteúdo" referenciado por uma ponteiro.  Mas, tal como o operador new (ou new[]) devolve o endereço da variável dinâmica criada, também ao operador delete (ou delete[]) deve ser passado o endereço da variável dinâmica.

[2 valores]

 Questão 1.3
Após a execução das seguintes instruções:
A* pa = new B;
B b;
B* pb = &b;
pa->g();
pb->g();
o que apareceria escrito no ecrã?
 F  Execução de A::g(). Execução de B::g().
 V  Execução de B::g(). Execução de B::g().
 F  Execução de A::g(). Execução de A::g().
A classe A é polimórfica, sendo a função g() virtual.  Assim, é o tipo do objecto apontado que determina a versão da função g() invocada.  Como em ambos os casos o objecto apontado é da classe B (derivada de A), a versão executada em ambos os casos é B::g().

 [1.5 valores]

 Questão 2
Assuma que está completamente definida a classe Tarefa :
class Tarefa {
  public:
    // Construtores:
    Tarefa();
    Tarefa(string etiqueta, string descrição, string notas);

    // Destrutor:
    virtual ~Tarefa() {}

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

    // Funções membro puramente virtuais:
    virtual int duração() const = 0;
    virtual int custoMaterial() const = 0;
    virtual int custoMaoDeObra() const = 0;

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

Assuma que está completamente definida uma classe ListaPonteiroParaTarefa cujas instâncias são listas de ponteiros para a classe Tarefa acima.  Como habitualmente, esta classe possui uma classe embutida Iterador para iterar ao longo dos seus itens.

Defina uma classe TarefaComposta tal que as suas instâncias sejam constituídas por um conjunto de ponteiros para várias tarefas que não têm dependências entre si.  Deve ser possível verificar se uma tarefa, dada a respectiva etiqueta, faz parte do conjunto de tarefas duma tarefa composta.  Deve também ser possível adicionar uma tarefa ao seu conjunto de tarefas, dado um ponteiro para a tarefa a adicionar.  A classe TarefaComposta não deve ser abstracta, mas sim concreta.

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

class TarefaComposta : public Tarefa {
  public:
    TarefaComposta();
    TarefaComposta(const string& etiqueta, const string& descrição,
                   const string& notas);
    bool contém(const string& etiqueta) const;
    void acrescenta(Tarefa* p_nova);
    int duração() const;
    int custoMaterial() const;
    int custoMaoDeObra() const;
  private:
    ListaPonteiroParaTarefa sub_tarefas;
}
 [4 valores]
Questão 3
Defina o método int TarefaComposta::duração() de modo a que a duração devolvida seja a maior das durações das tarefas existentes numa TarefaComposta.
int TarefaComposta::duração() const {
    typedef ListaPonteiroParaTarefa::IteradorConstante IteradorC;
    int duração_máxima = 0;
    for(IteradorC i = sub_tarefas.primeiro(); i != sub_tarefas.fim(); ++i)
        if(i.item()->duração() > duração_máxima)
            duração_máxima = i.item()->duração();
    return duração_máxima;
}
É de notar que esta solução não é a mais eficiente, pois o ciclo é executado sempre que a duração é necessária, mesmo que a tarefa composta não tenha sofrido alterações desde o último cálculo.  Como evitaria esta ineficiência?  A sua solução é 100% segura?  E se uma sub-tarefa da tarefa composta for alterada externamente (lembre-se que a tarefa composta limita-se a guardar ponteiros para as sub-tarefas)?

 [3 valores]

Defina o método da TarefaComposta que, dada uma etiqueta, verifica se existe nessa tarefa uma outra com essa etiqueta.

bool TarefaComposta::contém(const string& etiqueta) const {
    typedef ListaPonteiroParaTarefa::IteradorConstante IteradorC;
    for(IteradorC i = sub_tarefas.primeiro(); i != sub_tarefas.fim(); ++i)
        if(i.item()->etiqueta() == etiqueta)
            return true;
    return false;
}
 [3 valores]
 Questão 4
Assumindo que a estrutura No é definida do modo seguinte :
struct No {
    typedef int Item;

    // Não existe ponteiro para o nó anterior!
    No* seguinte;           // ponteiro para o nó seguinte.
    Item item;              // guarda o item propriamente dito.

    No(No* seg = 0, const Item& it = Item());
};

defina o procedimento
void insereOrdenadamente(No* inicial, No *final, const Item& item);
que insere ordenadamente um novo No contendo o item itemna posição correcta, de modo a manter a ordenação.  Assume-se que a ordem é crescente e que o conjunto de nós entre inicial e final é (simplesmente) ligado e está ordenado.

Assuma que os nós apontados por inicial e final são guardas, i.e., não contêm dados válidos.

void insereOrdenadamente(No* inicial, No* final, const Item& item) {
    // É necessário manter ponteiro para o nó anterior, pois a lista é simplesmente ligada:
    No* anterior = inicial;
    No* corrente = inicial->seguinte;
    while(corrente != final && corrente->item <= item) {
        anterior = corrente;
        corrente = corrente->seguinte;
    }
    // Aqui ou se encontrou um item maior no nó corrente ou todos os itens
    // da lista simplesmente ligada são menores ou iguais a item, sendo corrente o
    // nó final.  Em ambos os casos insere-se o novo nó antes do nó corrente:
    anterior->seguinte = new No(corrente, item);
}
Mas é possível simplificar esta função se se reconhecer que o ponteiro corrente deixou de ser necessário:
void insereOrdenadamente(No* inicial, No* final, const Item& item) {
    // É necessário manter ponteiro para o nó anterior, pois a lista é simplesmente ligada:
    No* anterior = inicial;
    while(anterior->seguinte != final && anterior->seguinte->item <= item)
        anterior = anterior->seguinte;

    // Aqui ou se encontrou um item maior no nó anterior->seguinte ou todos os itens
    // da lista simplesmente ligada são menores ou iguais a item, sendo anterior o
    // último nó.  Em ambos os casos insere-se o novo nó após o nó anterior:
    anterior->seguinte = new No(anterior->seguinte, item);
}

 [3 valores]
Questão 5
Explique sucintamente as vantagens da utilização de listas duplamente ligadas face às simplesmente ligadas.

As listas duplamente ligadas podem ser percorridas eficientemente em ambas as direcções.  Além disso tornam mais simples muitos tipos de manipulações, tais como inserções e remoções de itens.  No exemplo acima a inserção exige percorrer a lista com dois ponteiros desfasados de um nó.  Se a lista fosse duplamente ligada bastaria um único ponteiro.

[2 valores]