Resolução do Exame de 1 ª época
Programação Orientada para Objectos
IGE e ETI
2º semestre de 2000/2001
ISCTE
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
Basenã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_ed_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 chavestatic) 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:BaseeDerivada. Em seguida invoca-se a operaçãomé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étodométodo()da classeDerivada. Esse método insere no canal de saída o resultado da invocação de uma operação chamadanomeDoTipo(). 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 ponteirothis:
cout << this->nomeDoTipo() << endl;Acontece que o ponteiro
thisé sempre um ponteiro para a classe onde o código surge, neste casoDerivada. 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çãomain(), 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çõesnomeDoTipo()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
Basea partir da classeDerivada. 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
intcom o endereço de umfloat: 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)classe representando erro ao guardar, por definir.
throw /**/();
}
[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:
GestorDeCatálogo, de modo a que esta esteja ordenada primeiro
por referência e depois por preço.
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_componentestipo_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);classe representando erro ao carregar, por definir.
int número_de_componentes;
if(not (entrada >> número_de_componentes))
throw /**/();aqui é fundamental limpar o canal até ao fim de linha inclusive!
//classe representando erro ao carregar, por definir.
while(número_de_componentes-- != 0) {
string nome_do_tipo;
if(not (entrada >> nome_do_tipo))
throw /**/();aqui é fundamental limpar o canal até ao fim de linha inclusive!
//classe representando erro ao carregar, por definir.
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 /**/();
}
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 estruturausada para representar os elos de uma cadeia duplamente ligada (não-circular) com guardas.
struct Elo {typedef int Item;o item propriamente dito.
Item item; //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());};
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,função que verifica se entre inicial e final há caminhos em ambas as
Elo::Item const& item)
{
assert(inicial != 0 and final != 0 and
/*
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:Explique as alterações na definição da classe
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:
//A matriz dos itens passa a ser a nova matriz construída (já com os itens antigos):
delete[] itens;
//itens = novos_itens;}
int main()
{...}
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.
[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 classePilhaIntdentro de um (ou dois) espaços nominativos:
namespace OBaratoSaiCaro {...
namespace Contentores {
//Definições do ficheiropilha_int.H.
}
}
[cotação: 1,5]