Pular para conteúdo

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:

envia seguro
~Conexao

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:

virtual ~Conexao() { std::cout << "~Conexao\n"; }

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) devolve true se o elevador pôde atender e false caso contrário:
    • destino fora da faixa → devolve false e 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.
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_ com 0 em vez de andar_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:

private:
    std::vector<int> pontos_;

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.

double total(const std::vector<std::unique_ptr<Frete>>& fretes, double peso);
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 accumulate com 0 em vez de 0.0 — o acumulador vira int, e cada parcela é truncada no meio do caminho;
  • esquecer o const em valor(...) const na derivada: a assinatura deixa de bater com a da base, o override não compila (e sem override seria pior, porque criaria silenciosamente uma função nova);
  • tentar copiar o unique_ptr dentro da lambda — receba-o por const&.

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 devolve false e não mexe no parâmetro.
  • sai() precisa liberar o nó removido.
  • Atenção: ao sair o último elemento, fim_ precisa voltar a nullptr. Esse ponto será avaliado — se ele ficar apontando para memória liberada, a próxima chamada a entra corrompe 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:

  1. Em entra, quando a fila está vazia, os dois ponteiros precisam apontar para o nó novo. Escrever só fim_ = novo deixa inicio_ nulo, e a fila nasce quebrada.
  2. Em sai, quando o último elemento vai embora, fim_ ainda aponta para o nó que acabou de ser liberado. Sem o fim_ = nullptr, o próximo entra faria fim_->prox = novo em 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->item depois do delete velho — o nó já não existe mais. Copie o item antes;
  • esquecer o delete e vazar um nó a cada sai();
  • devolver "" em vez de nullopt para 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.