Tracks
/
C++
C++
/
Ejercicios
/
Doctor Data
Doctor Data

Doctor Data

Ejercicio de aprendizaje

Introducción

En C++, las declaraciones a menudo se separan de las definiciones. Las declaraciones se agrupan en los llamados archivos de cabecera, y las implementaciones respectivas se colocan en archivos fuente. Puedes pensar en los archivos de cabecera como una API. El archivo de cabecera te dice qué ofrece una base de código, sin entrar en los detalles de cómo.

Cabeceras y archivos fuente

La extensión de archivo más común para los archivos de cabecera es .h. Algunos proyectos usan .hpp o prescinden por completo de la extensión.

Las definiciones están en un archivo .cpp aparte. Para reunir las partes, el archivo fuente empieza por incluir el archivo de cabecera correspondiente.

Si quieres escribir una biblioteca llamada «quick_math» que ofrece una función «super_root» que quieres usar a menudo, los archivos se verían así:

// 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;
}

Si necesitas incluir una cabecera que solo requiere la implementación, la línea #include correspondiente solo es necesaria en el archivo fuente. Todo lo que se incluye en la cabecera también está disponible en el archivo .cpp, como la biblioteca string del ejemplo de abajo. Atención: el ; es necesario después de la declaración en el archivo de cabecera, pero no después de la definición en el archivo fuente.

Note

Muchos ejercicios de C++ en Exercism empiezan con dos archivos casi vacíos: cabecera y fuente. Tienes que revisar el archivo *_test.cpp para ver los nombres y los espacios de nombres de las funciones esperadas y así poder resolver el ejercicio.

Clases y cabeceras

Las clases pueden volverse muy complejas y su relación con la división entre cabecera y fuente puede resultar confusa. Una organización posible es mantener todos los detalles de la implementación en el archivo fuente y todas las declaraciones y variables miembro en la cabecera:

// 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;}

Cuando la cabecera se usa como resumen de la API, es ahí donde uno buscaría información como los valores predeterminados. Por eso, el valor predeterminado del parámetro size del constructor se define en la cabecera y no en la implementación. Las definiciones en el archivo fuente llevan como prefijo el espacio de nombres robots y el tipo de clase Flower.

Otra opción de organización es una biblioteca header only, que no tiene ningún archivo .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;}
    };
}

Los proyectos pueden usar combinaciones de estas organizaciones y hay mucho debate sobre cuál podría ser la mejor opción para cada caso de uso.

Guardas de inclusión

Es posible que hayas notado la línea #pragma once en el archivo de cabecera de ejemplo de arriba. Esto se llama guarda de inclusión y garantiza que el contenido del archivo se incluya una sola vez durante la compilación para evitar errores. Existe otra variación más compleja de una guarda de inclusión que empieza con #ifndef y termina con #endif. Cumple la misma función y su uso se muestra en el ejemplo de la clase Flower de arriba.

Instrucciones

Empiezas tu primer día en una empresa australiana llamada «Doctor Data», que se especializa en la recuperación de información. Superaste tu entrevista de trabajo gracias a tus conocimientos de C++ y de las sondas de von Neumann. Como ya has visto muchos archivos de prueba, tu nuevo jefe quiere que recrees los respectivos archivos fuente y de encabezado a partir de un código de prueba que la empresa recuperó hace poco.

En este ejercicio, vas a recrear archivos perdidos. Por ahora, el contenido de los archivos es basura inservible.

Note

El flujo de trabajo de este ejercicio de concepto es muy similar a la estructura de los ejercicios de práctica de Exercism. El texto de introducción del ejercicio es solo una parte de la especificación. El archivo de prueba es tu guía definitiva para resolver un problema determinado. Debido a cómo funciona la compilación de C++, es posible que los resultados de las pruebas no aparezcan en la interfaz web hasta que hayas implementado una versión mínima de cada clase, función y enumeración que las pruebas requieren. Si recibes errores de compilación, es posible que desaparezcan una vez que hayas completado todas las tareas y pruebas.

Tienes cuatro tareas, todas relacionadas con recuperar el código perdido en los archivos de encabezado y fuente.

1. Intenta extraer las primeras pistas

Observas dos líneas recuperadas de los archivos de prueba:

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

Tu ojo agudo reconoció al instante un espacio de nombres heaven. También ves que debe haber una clase llamada Vessel. Al parecer, el constructor se puede llamar con dos o tres argumentos. El primer argumento parece ser de tipo string y el segundo es un número. Es posible inicializar la clase Vessel con un tercer argumento. El tercer argumento proviene de un espacio de nombres star_map. Es un enumerador de tipo System. Incluso obtuviste una de las enumeraciones: BetaHydri.

Prepara los archivos fuente y de encabezado con la información que descubriste. Necesitas dos espacios de nombres: heaven y star_map. El espacio de nombres heaven tiene una clase Vessel, que se puede llamar con dos o tres argumentos. La enumeración System debe colocarse en el espacio de nombres star_map y tiene un enumerador BetaHydri. Mantente atento a otras enumeraciones, para que puedas actualizar la enumeración System constantemente.

2. Encuentra más detalles

Descubres más líneas de la prueba:

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);

Las líneas de prueba recién encontradas revelan otra función miembro de la clase Vessel: replicate. Esta función recibe un string y devuelve otra instancia de Vessel. Te haces una idea del valor predeterminado del tercer argumento del constructor de la tarea anterior. También ves dos variables miembro públicas de la clase Vessel y la especificación de la función miembro replicate.

Agrega la función replicate y las variables miembro públicas current_system y generation a los archivos de encabezado y fuente.

3. Examina el funcionamiento interno

Encuentras algunas líneas más interesantes en los archivos de prueba 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);

Al parecer, la clase Vessel tiene una variable miembro busters, que se puede cambiar con las dos funciones miembro de la clase make_buster y shoot_buster. Hasta que aparezca más información, supones que la función make_buster devuelve void. Como hay una prueba para el valor de retorno de shoot_buster, asumes que la función devuelve un bool.

Agrega las dos funciones y la variable miembro a la clase Vessel. Sigue buscando otros enumeradores de System.

4. Completa el panorama

Mientras revisas los archivos de prueba, encuentras solo dos secciones de código sin descubrir:

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));

Ves dos funciones que no son miembros de la clase Vessel, ya que no se llaman con una instancia. get_older_bob compara dos instancias de Vessel y devuelve un string. in_the_same_system compara dos instancias de Vessel y devuelve un bool.

Implementa las últimas funciones que faltan a partir de las líneas recuperadas de arriba.

Editar en GitHub El enlace se abre en una ventana o una pestaña nuevas
C++ Exercism

¿Todo listo para empezar Doctor Data?

Regístrate en Exercism para aprender y dominar C++ con 19 conceptos100 ejercicios y mentoría humana real, todo gratis.