Simulado — Avaliação 2¶
Este simulado tem o mesmo formato da Avaliação 2 de verdade: manuscrito, 90 minutos, uma questão de leitura de código e quatro de escrever código, cobrindo as Unidades 4, 5 e 6 (capítulos 8 a 13). Veja as regras da Avaliação 2 para peso, consulta e critério de correção.
Como usar este simulado
Resolva no papel, com um cronômetro de 90 minutos e só a sua folha de consulta ao lado. Em todas as questões a declaração da classe já vem dada — você escreve apenas os corpos pedidos, como na prova.
Cada questão traz a resposta logo abaixo, recolhida. Só abra depois de ter escrito a sua.
Aqui as ferramentas prontas são bem-vindas
Ao contrário da Avaliação 1, std::sort, std::find, std::count_if, std::accumulate e std::max_element podem e devem ser usados. O que se avalia agora é o desenho: encapsulamento, RAII e polimorfismo corretos.
Questão 1 — Leitura de código¶
class Conexao {
public:
explicit Conexao(std::string destino) : destino_(std::move(destino)) {}
virtual void envia() const { std::cout << "envia simples\n"; }
~Conexao() { std::cout << "~Conexao\n"; }
protected:
std::string destino_;
};
class ConexaoSegura : public Conexao {
public:
explicit ConexaoSegura(std::string destino)
: Conexao(std::move(destino)), chaves_{"k1", "k2"} {}
void envia() const override { std::cout << "envia seguro\n"; }
~ConexaoSegura() { std::cout << "~ConexaoSegura\n"; }
private:
std::vector<std::string> chaves_;
};
int main() {
Conexao* c = new ConexaoSegura("servidor");
c->envia();
delete c;
}
a) Escreva exatamente o que o programa imprime.
b) A chamada c->envia() escolhe a versão certa, mas a destruição não. Explique por quê, e diga o que acontece com o campo chaves_.
c) Qual é a mudança mínima que corrige isso? E se o código usasse std::unique_ptr<Conexao> em vez do ponteiro cru, o problema desapareceria?
Resposta
a) O programa imprime:
Repare no que não aparece: a linha ~ConexaoSegura.
b) São dois mecanismos diferentes, e só um deles está ligado aqui.
envia() é virtual, então a chamada é resolvida em tempo de execução, pelo tipo real do objeto — que é ConexaoSegura. Por isso sai envia seguro. É o polimorfismo funcionando como esperado.
O destrutor de Conexao não é virtual. Quando você escreve delete c, e c é um Conexao*, o compilador só sabe olhar para o tipo do ponteiro: chama ~Conexao() e pronto. O destrutor da derivada nunca roda, e o objeto é destruído pela metade.
A consequência para chaves_: ele é um membro de ConexaoSegura, e membros são destruídos como parte da destruição da derivada — que não aconteceu. O vector nunca é destruído, e a memória que ele havia alocado vaza. Note como o programa não dá erro nenhum: ele roda, imprime, termina. É o que torna esse bug perigoso — ele é silencioso.
Vale saber que o g++ avisa, se você compilar com -Wall:
warning: deleting object of polymorphic class type 'Conexao' which has
non-virtual destructor might cause undefined behavior [-Wdelete-non-virtual-dtor]
c) A mudança mínima é uma palavra — tornar o destrutor da base virtual:
Com isso a saída passa a ser envia seguro, ~ConexaoSegura, ~Conexao: a destruição acontece da derivada para a base, na ordem correta.
E não — trocar para unique_ptr não resolveria. Esta é a parte que separa quem entendeu de quem decorou: std::unique_ptr<Conexao> chama delete no ponteiro que guarda, que é um Conexao*, exatamente como o código original. Sem destrutor virtual, o vazamento continua igual. O ponteiro inteligente cuida de quando destruir, não de qual destrutor chamar.
A regra: toda classe pensada para ser herdada e usada por ponteiro de base precisa de destrutor virtual.
Questão 2 — Elevador¶
Dada a declaração:
class Elevador {
public:
Elevador(int andar_min, int andar_max);
bool vai_para(int destino);
int andar() const { return andar_; }
int viagens() const { return viagens_; }
private:
int andar_;
int andar_min_;
int andar_max_;
int viagens_;
};
Escreva o corpo do construtor e do método vai_para.
- O construtor guarda a faixa de andares e deixa o elevador parado no andar mínimo, com zero viagens. Use lista de inicialização. Pode assumir
andar_min < andar_max. vai_para(destino)devolvetruese o elevador pôde atender efalsecaso contrário:- destino fora da faixa → devolve
falsee não altera nada (nem o andar, nem a contagem); - destino igual ao andar atual → devolve
true, mas não conta viagem (o elevador não se moveu); - caso contrário → move, conta uma viagem e devolve
true.
- destino fora da faixa → devolve
Resposta
Elevador::Elevador(int andar_min, int andar_max)
: andar_(andar_min), andar_min_(andar_min),
andar_max_(andar_max), viagens_(0) {}
bool Elevador::vai_para(int destino) {
if (destino < andar_min_ || destino > andar_max_) {
return false; // invalido: sai antes de mexer em qualquer campo
}
if (destino == andar_) {
return true; // ja esta la: atendido, mas nao houve viagem
}
andar_ = destino;
viagens_++;
return true;
}
O que se avalia aqui: a ordem das guardas. As duas verificações vêm antes de qualquer atribuição, e é isso que garante o invariante da classe — andar_ nunca sai da faixa, e viagens_ nunca conta um movimento que não houve. Um método que altera o estado e só depois descobre que o pedido era inválido deixa o objeto corrompido, mesmo que devolva false direitinho.
Erros comuns:
- incrementar
viagens_antes de checar a faixa; - tratar "destino igual ao atual" como erro, devolvendo
false— o pedido foi atendido, ele só não exigiu movimento; - inicializar
andar_com0em vez deandar_min_: se a faixa começa no subsolo (-2), o elevador nasce fora dela.
Questão 3 — Ranking e a regra do zero¶
Esta classe foi escrita por alguém que ainda pensava em C:
class Ranking {
public:
Ranking() : pontos_(new int[100]), n_(0) {}
~Ranking() { delete[] pontos_; }
void registra(int p);
bool melhor(int& pontuacao) const;
private:
int* pontos_;
int n_;
};
a) Reescreva apenas a seção privada, seguindo a regra do zero, e diga quais funções membro passam a não ser mais necessárias.
b) Escreva o corpo de registra e de melhor na sua versão. melhor() escreve a maior pontuação já registrada no parâmetro e devolve true; devolve false se nenhuma foi registrada ainda.
c) Em uma frase: por que a versão acima quebra se alguém escrever Ranking b = a;?
Resposta
a) A seção privada inteira vira um campo:
Somem o destrutor, o construtor (o vector já nasce vazio) e — o mais importante — a necessidade de escrever construtor de cópia e operador de atribuição. É a regra do zero: como vector já é RAII e sabe se copiar e se destruir, as versões que o compilador gera sozinho já estão corretas.
b)
void Ranking::registra(int p) {
pontos_.push_back(p);
}
bool Ranking::melhor(int& pontuacao) const {
if (pontos_.empty()) {
return false;
}
pontuacao = *std::max_element(pontos_.begin(), pontos_.end());
return true;
}
Repare no * antes do std::max_element: ele devolve um iterador, não o valor — desreferenciar é obrigatório. E a checagem de vazio vem primeiro, porque max_element de uma faixa vazia devolve end(), que não pode ser desreferenciado.
c) Porque a cópia gerada pelo compilador copia o ponteiro, não o array: a.pontos_ e b.pontos_ passam a apontar para o mesmo bloco, e quando os dois forem destruídos cada um dará delete[] nele — double free. Era exatamente para evitar isso que a regra dos 3 existe; a regra do zero evita o problema inteiro, em vez de remendá-lo.
Questão 4 — Frete¶
Dadas as declarações:
class Frete {
public:
virtual double valor(double peso) const = 0;
virtual ~Frete() = default;
};
class FreteFixo : public Frete {
public:
explicit FreteFixo(double valor) : valor_(valor) {}
double valor(double peso) const override;
private:
double valor_;
};
class FretePorPeso : public Frete {
public:
explicit FretePorPeso(double por_kg) : por_kg_(por_kg) {}
double valor(double peso) const override;
private:
double por_kg_;
};
Escreva:
a) Os corpos dos dois valor. FreteFixo cobra sempre o mesmo, independente do peso; FretePorPeso cobra por_kg * peso.
b) A função livre abaixo, que soma quanto todos os fretes cobrariam para um mesmo peso. Um vector vazio dá 0.0.
Resposta
a)
double FreteFixo::valor(double) const {
return valor_;
}
double FretePorPeso::valor(double peso) const {
return por_kg_ * peso;
}
Em FreteFixo o parâmetro não é usado. Você pode omitir o nome (double) para deixar isso explícito e evitar aviso do compilador — mas manter double peso também está certo.
b)
double total(const std::vector<std::unique_ptr<Frete>>& fretes, double peso) {
return std::accumulate(fretes.begin(), fretes.end(), 0.0,
[peso](double acc, const std::unique_ptr<Frete>& f) {
return acc + f->valor(peso);
});
}
Um laço for range acumulando numa variável vale a mesma nota — o accumulate é preferível por ser mais direto, não obrigatório.
O que se avalia aqui: o valor() chamado através de f-> é resolvido pelo tipo real de cada objeto, e é por isso que a mesma linha cobra de um jeito para FreteFixo e de outro para FretePorPeso. O vector guarda unique_ptr<Frete> (e não Frete) justamente porque polimorfismo exige ponteiro ou referência: um vector<Frete> nem compilaria, já que Frete é abstrata.
Erros comuns:
- começar o
accumulatecom0em vez de0.0— o acumulador viraint, e cada parcela é truncada no meio do caminho; - esquecer o
constemvalor(...) constna derivada: a assinatura deixa de bater com a da base, ooverridenão compila (e semoverrideseria pior, porque criaria silenciosamente uma função nova); - tentar copiar o
unique_ptrdentro da lambda — receba-o porconst&.
Questão 5 — FilaEncadeada¶
Dados:
struct No {
std::string item;
No* prox;
};
class FilaEncadeada {
public:
FilaEncadeada() : inicio_(nullptr), fim_(nullptr) {}
~FilaEncadeada(); // ja implementado, nao precisa escrever
void entra(const std::string& s);
bool sai(std::string& item);
private:
No* inicio_;
No* fim_;
};
Escreva o corpo de entra e de sai. A fila é FIFO: quem entra primeiro sai primeiro. entra insere no fim; sai remove do início e entrega o item pelo parâmetro de referência, devolvendo true.
sai()numa fila vazia devolvefalsee não mexe no parâmetro.sai()precisa liberar o nó removido.- Atenção: ao sair o último elemento,
fim_precisa voltar anullptr. Esse ponto será avaliado — se ele ficar apontando para memória liberada, a próxima chamada aentracorrompe a fila.
Resposta
void FilaEncadeada::entra(const std::string& s) {
No* novo = new No{s, nullptr};
if (fim_ == nullptr) { // fila vazia: o novo no e o inicio e o fim
inicio_ = novo;
fim_ = novo;
} else {
fim_->prox = novo;
fim_ = novo;
}
}
bool FilaEncadeada::sai(std::string& item) {
if (inicio_ == nullptr) {
return false;
}
No* velho = inicio_;
item = velho->item; // copia ANTES de liberar o no
inicio_ = velho->prox;
if (inicio_ == nullptr) { // saiu o ultimo: a fila esvaziou
fim_ = nullptr;
}
delete velho;
return true;
}
Os dois pontos que decidem a questão são os casos de borda, e os dois envolvem a fila ficando vazia ou deixando de estar:
- Em
entra, quando a fila está vazia, os dois ponteiros precisam apontar para o nó novo. Escrever sófim_ = novodeixainicio_nulo, e a fila nasce quebrada. - Em
sai, quando o último elemento vai embora,fim_ainda aponta para o nó que acabou de ser liberado. Sem ofim_ = nullptr, o próximoentrafariafim_->prox = novoem memória já devolvida — comportamento indefinido, e o tipo de bug que só aparece muito depois, em outro lugar do programa.
Erros comuns:
- ler
velho->itemdepois dodelete velho— o nó já não existe mais. Copie o item antes; - esquecer o
deletee vazar um nó a cadasai(); - devolver
""em vez denulloptpara a fila vazia: string vazia é um item perfeitamente válido, então ela não pode significar "não tem nada".
Sobre o destrutor, que veio pronto: ele precisa percorrer a fila liberando nó por nó. Esta é uma das poucas situações em que escrever destrutor é o certo — a lista foi encadeada à mão com new, e não existe nenhum tipo da biblioteca cuidando dela. Por ter destrutor próprio, a classe cai na regra dos 3: copiá-la sem cuidado causaria double free, e é por isso que a cópia costuma ser proibida em estruturas assim.