Percursos
/
C++
C++
/
Exercícios
/
Doutor Dados
Doutor Dados

Doutor Dados

Exercício de aprendizagem

Introdução

Em C++, as declarações estão muitas vezes separadas das definições. As declarações agrupam-se em ficheiros de cabeçalho, e as respetivas implementações ficam em ficheiros de código-fonte. Podes pensar nos ficheiros de cabeçalho como uma API. O ficheiro de cabeçalho diz-te o que um projeto de código tem para oferecer, sem entrar nos detalhes de como.

Cabeçalho e código-fonte

A extensão de ficheiro mais comum para ficheiros de cabeçalho é .h. Alguns projetos usam .hpp ou dispensam completamente a extensão.

As definições ficam num ficheiro .cpp à parte. Para reunir as partes, o ficheiro de código-fonte começa por incluir o respetivo ficheiro de cabeçalho.

Se quiseres escrever uma biblioteca chamada "quick_math" que oferece uma função "super_root" que queres usar com frequência, os ficheiros ficariam assim:

// A file named quick_math.h
#pragma once
namespace quick_math {
    double super_root(double x, int n);
}
// A file named quick_math.cpp
#include "quick_math.h"
#include <cmath>
double quick_math::super_root(double x, int n) {
    while(n) { x = std::sqrt(x), --n;}
    return x;
}

Se precisares de incluir um cabeçalho que só é necessário para a implementação, a respetiva linha #include só é precisa no ficheiro de código-fonte. Tudo o que está incluído no cabeçalho também fica disponível no ficheiro .cpp, como a biblioteca string no exemplo abaixo. Atenção: o ; é necessário depois da declaração no ficheiro de cabeçalho, mas não depois da definição no ficheiro de código-fonte.

Note

Muitos exercícios de C++ no Exercism começam com dois ficheiros quase vazios: o cabeçalho e o código-fonte. Tens de verificar o ficheiro *_test.cpp para conhecer os nomes e os namespaces das funções esperadas, de modo a resolver o exercício.

Classes e cabeçalhos

As classes podem tornar-se muito complexas e a sua relação com a divisão entre cabeçalho e código-fonte pode ser confusa. Uma organização possível é manter todos os detalhes de implementação no ficheiro de código-fonte e todas as declarações e variáveis membro no cabeçalho:

// A file named robot_flower.h
#if !defined(ROBOT_FLOWER_H)
#define ROBOT_FLOWER_H
#include <string>
namespace robots {
    class Flower {
        private:
            bool needs_water{};
            int size{};
            std::string name{};
        public:
            Flower(std::string name, int size = 0);
            void give_water();
            std::string get_name();
            int get_size();
            void start_next_day();
    };
}
#endif
// A file named robot_flower.cpp
#include "robot_flower.h"
robots::Flower::Flower(std::string name, int size) {this->name = "Robotica " + name; this->size = size;}
void robots::Flower::start_next_day() {if (!needs_water) ++size; needs_water = true;}
std::string robots::Flower::get_name() {return name;}
int robots::Flower::get_size() {return size;}

Quando o cabeçalho serve de visão geral da API, é aí que alguém procuraria informações como os valores predefinidos. O valor predefinido do parâmetro size do construtor é, por isso, tratado no cabeçalho e não na implementação. As definições no ficheiro de código-fonte são prefixadas com o namespace robots e o tipo de classe Flower.

Outra opção de organização é uma biblioteca header only, que não tem sequer um ficheiro .cpp:

// A file named robot_flower.h
#pragma once
#include <string>
namespace robots {
    class Flower {
        private:
            bool needs_water{};
            int size{};
            std::string name{};
        public:
            Flower(std::string name, int size = 0) {this->name = "Robotica " + name; this->size = size;}
            void give_water() {needs_water = false;}
            std::string get_name() {return name;}
            int get_size() {return size;}
            void start_next_day() {if (!needs_water) ++size; needs_water = true;}
    };
}

Os projetos podem usar combinações destas organizações e há muita discussão sobre qual será a mais adequada para cada caso de uso.

Guardas de inclusão

Deves ter reparado na linha #pragma once no ficheiro de cabeçalho de exemplo acima. Isto chama-se uma guarda de inclusão e garante que o conteúdo do ficheiro é incluído apenas uma vez durante a compilação, para evitar erros. Existe outra variante, mais complexa, de uma guarda de inclusão, que começa com #ifndef e termina com #endif. Tem o mesmo propósito e o seu uso é mostrado no exemplo da classe Flower acima.

Instruções

Começas o teu primeiro dia numa empresa australiana chamada "Doctor Data", especializada em recuperação de informação. Arrasaste na entrevista de emprego graças aos teus conhecimentos de C++ e de sondas de von Neumann. Como já viste muitos ficheiros de testes, o teu novo chefe quer que recries os respetivos ficheiros fonte e de cabeçalho a partir de algum código de teste que a empresa recuperou recentemente.

Neste exercício, vais recriar ficheiros perdidos. Atualmente, o conteúdo dos ficheiros é lixo inutilizável.

Note

O fluxo de trabalho deste exercício de conceito é muito semelhante à estrutura dos exercícios de prática do Exercism. O texto de introdução do exercício é apenas uma parte da especificação. O ficheiro de testes é o teu guia definitivo para resolver um determinado problema. Devido à forma como a compilação de C++ funciona, os resultados dos testes podem não aparecer na interface web enquanto não tiveres implementado uma versão mínima de todas as classes, funções e enumerações exigidas pelos testes. Se receberes erros de compilação, estes podem desaparecer depois de teres tratado de todas as tarefas e testes.

Tens quatro tarefas, todas relacionadas com a recuperação do código perdido nos ficheiros de cabeçalho e fonte.

1. Tenta extrair as primeiras pistas

Olhas para duas linhas recuperadas dos ficheiros de testes:

heaven::Vessel bob{"Robert Johansson", 1};
heaven::Vessel will{"Riker", 2, star_map::System::BetaHydri};

O teu olhar atento reconheceu imediatamente um namespace heaven. Vês também que deve haver uma classe chamada Vessel. Pelos vistos, o construtor pode ser chamado com dois ou três argumentos. O primeiro argumento parece ser do tipo string e o segundo é um número. É possível inicializar a classe Vessel com um terceiro argumento. O terceiro argumento vem de um namespace star_map. É um enumerador do tipo System. Até já descobriste uma das enumerações: BetaHydri.

Prepara os ficheiros fonte e de cabeçalho com a informação que descobriste. Precisas de dois namespaces: heaven e star_map. O namespace heaven tem uma classe Vessel, que pode ser chamada com dois ou três argumentos. O enum System deve ser colocado no namespace star_map e tem uma enumeração BetaHydri. Fica atento a outras enumerações, para poderes atualizar constantemente o enum System.

2. Encontra mais detalhes

Desenterras mais linhas do teste:

heaven::Vessel bob{"Robert Johansson", 1};
REQUIRE(bob.current_system == star_map::System::Sol);
REQUIRE(bob.generation == 1);
heaven::Vessel bob5 = bob.replicate("Mario");
REQUIRE(bob5.current_system == star_map::System::Sol);
REQUIRE(bob5.generation == 2);

As linhas de teste que acabaste de encontrar revelam outra função membro da classe Vessel: replicate. Esta função recebe uma string e devolve outra instância de Vessel. Ficas com uma ideia do valor predefinido do terceiro argumento do construtor da tarefa anterior. Vês também duas variáveis membro públicas da classe Vessel e a especificação da função membro replicate.

Adiciona a função replicate e as variáveis membro públicas current_system e generation aos ficheiros de cabeçalho e fonte.

3. Observa o funcionamento interno

Encontras mais algumas linhas interessantes nos ficheiros de testes recuperados:

heaven::Vessel bob6{"Homer", 3, star_map::System::EpsilonEridani};
REQUIRE(bob6.busters == 0);
bob6.make_buster();
REQUIRE(bob6.busters == 1);
bool success = bob6.shoot_buster();
REQUIRE(success);
REQUIRE(bob6.busters == 0);
success = bob6.shoot_buster();
REQUIRE_FALSE(success);

Ao que parece, a classe Vessel tem uma variável membro busters, que pode ser alterada com as duas funções membro da classe make_buster e shoot_buster. Enquanto não surge mais informação, arriscas um palpite de que a função make_buster devolve void. Como há um teste para o valor devolvido de shoot_buster, assumes que a função devolve um bool.

Adiciona as duas funções e a variável membro à classe Vessel. Continua à procura de outros enumeradores de System.

4. Completa o quadro

Enquanto analisas os ficheiros de testes, encontras apenas duas secções de código por descobrir:

heaven::Vessel bob1{"Bob", 1, star_map::System::AlphaCentauri};
heaven::Vessel marv{"Marvin", 2, star_map::System::DeltaEridani};
heaven::Vessel milo{"Milo", 3, star_map::System::DeltaEridani};
heaven::Vessel howie{"Howard", 4, star_map::System::Omicron2Eridani};

REQUIRE("Bob" == heaven::get_older_bob(bob1, marv));
REQUIRE(heaven::in_the_same_system(marv, milo));
REQUIRE_FALSE(heaven::in_the_same_system(marv, howie));

Vês duas funções que não são membros da classe Vessel, pois não são chamadas com uma instância. get_older_bob compara duas instâncias de Vessel e devolve uma string. in_the_same_system compara duas instâncias de Vessel e devolve um bool.

Implementa as últimas funções em falta a partir das linhas recuperadas acima.

Editar via GitHub A ligação abre numa nova janela ou separador
C++ Exercism

Estás pronto para começar Doutor Dados?

Inscreve-te no Exercism para aprenderes e dominares C++ com 19 conceitos100 exercícios, e mentoria humana real, tudo grátis.