Треки
/
C++
C++
/
Вправи
/
Доктор Даних
Доктор Даних

Доктор Даних

Навчальна вправа

Вступ

У C++ оголошення часто відокремлені від визначень. Оголошення згруповані в так звані заголовні файли, а відповідні реалізації розміщено у файлах вихідного коду. Заголовні файли можна уявляти собі як API. Заголовний файл розповість, що пропонує кодова база, не заглиблюючись у подробиці як.

Заголовні та вихідні файли

Найпоширеніше розширення для заголовних файлів - .h. Деякі проєкти використовують .hpp або взагалі обходяться без розширення.

Визначення розміщені в окремому файлі .cpp. Щоб знову поєднати частини, вихідний файл починається з підключення відповідного заголовного файлу.

Якщо ми хочемо написати бібліотеку під назвою «quick_math», яка пропонує функцію «super_root», яку ми хочемо часто використовувати, файли матимуть такий вигляд:

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

Якщо потрібно підключити заголовок, який використовує лише реалізація, то відповідний рядок #include знадобиться лише у файлі вихідного коду. Усе, що підключено в заголовку, також доступне у файлі .cpp, як-от бібліотека string у прикладі нижче. Увага: символ ; потрібен після оголошення в заголовному файлі, але не після визначення у файлі вихідного коду.

Note

Багато вправ із C++ на Exercism починаються з двох майже порожніх файлів: заголовного та вихідного. Для розвʼязання вправи перегляньте файл *_test.cpp, щоб побачити назви та простори імен очікуваних функцій.

Класи та заголовки

Класи можуть ставати дуже складними, і їхній звʼязок із поділом на заголовки та вихідний код може заплутувати. Один із можливих варіантів компонування - тримати всі подробиці реалізації у файлі вихідного коду, а всі оголошення та змінні-члени - у заголовку:

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

Коли заголовок використовують як огляд API, саме там шукатимуть таку інформацію, як типові значення. Тому типове значення параметра size конструктора задають у заголовку, а не в реалізації. Визначення у файлі вихідного коду мають префікс із простору імен robots та типу класу Flower.

Ще один варіант компонування - бібліотека лише із заголовком, яка взагалі не має файлу .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;}
    };
}

Проєкти можуть поєднувати ці варіанти компонування, і точаться чималі дискусії про те, що найкраще підходить для кожного випадку.

Захист від повторного включення

Можливо, ми помітили рядок #pragma once у наведеному вище прикладі заголовного файлу. Це називається захистом від повторного включення. Він гарантує, що вміст файлу підключається під час компіляції лише один раз, щоб уникнути помилок. Існує ще один, складніший різновид захисту від повторного включення, який починається з #ifndef і закінчується #endif. Він слугує тій самій меті, і його застосування показано в наведеному вище прикладі класу Flower.

Вказівки

Ми починаємо свій перший робочий день в австралійській компанії «Doctor Data», яка спеціалізується на відновленні інформації. Ми блискуче склали співбесіду завдяки знанню C++ і зондів фон Неймана. Оскільки ми вже бачили чимало тестових файлів, новий керівник хоче, щоб ми відтворили відповідні вихідні та заголовкові файли з тестового коду, який компанія нещодавно відновила.

У цій вправі ми відтворимо втрачені файли. Наразі вміст файлів - непридатні для використання сміттєві дані.

Note

Робочий процес цієї концептуальної вправи дуже схожий на структуру практичних вправ Exercism. Вступний текст вправи - лише одна частина специфікації. Тестовий файл - це вичерпний орієнтир для розвʼязання задачі. Через те, як працює компіляція C++, результати тестів можуть не зʼявитися у вебінтерфейсі, доки не буде реалізовано мінімальну версію кожного класу, функції та переліку, яких вимагають тести. Якщо зʼявляться помилки компіляції, вони можуть зникнути після того, як ми виконаємо всі завдання й тести.

На нас чекають чотири завдання, усі повʼязані з відновленням втраченого коду в заголовкових файлах і файлах вихідного коду.

1. Спробуймо видобути перші підказки

Погляньмо на два відновлені рядки з тестових файлів:

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

Наш пильний погляд одразу помітив простір імен heaven. Ми також бачимо, що має бути клас із назвою Vessel. Конструктор, очевидно, можна викликати з двома або трьома аргументами. Перший аргумент, схоже, має тип string, а другий - число. Клас Vessel можна ініціалізувати й третім аргументом. Третій аргумент походить із простору імен star_map. Це елемент переліку типу System. Нам навіть дістався один з елементів переліку: BetaHydri.

Підготуйте вихідні та заголовкові файли зі знайденою інформацією. Потрібні два простори імен: heaven і star_map. У просторі імен heaven є клас Vessel, який можна викликати з двома або трьома аргументами. Перелік System треба розмістити в просторі імен star_map, і він має елемент BetaHydri. Стежте за іншими елементами переліку, щоб постійно оновлювати перелік System.

2. Знайдімо більше деталей

Ми відкриваємо ще кілька рядків із тесту:

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

Нові знайдені рядки тесту відкривають ще одну функцію-член класу Vessel: replicate. Ця функція приймає рядок тексту (англ. string) і повертає інший примірник Vessel. Ми отримуємо уявлення про типове значення третього аргументу конструктора з попереднього завдання. Ми також бачимо дві відкриті змінні-члени класу Vessel і специфікацію функції-члена replicate.

Додайте функцію replicate та відкриті змінні-члени current_system і generation до заголовкового та вихідного файлів.

3. Зазирнімо у внутрішній механізм

Ми знаходимо ще кілька цікавих рядків у відновлених тестових файлах:

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

Очевидно, клас Vessel має змінну-член busters, яку можна змінювати двома функціями-членами класу: make_buster і shoot_buster. Доки не знайдеться інша інформація, припускаємо, що функція make_buster повертає void. Оскільки для поверненого значення shoot_buster є тест, припускаємо, що функція повертає bool.

Додайте ці дві функції та змінну-член до класу Vessel. Продовжуйте шукати інші елементи переліку System.

4. Завершімо картину

Під час перегляду тестових файлів ми знаходимо лише дві ще не охоплені ділянки коду:

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

Ми бачимо дві функції, які не є членами класу Vessel, бо їх не викликають через примірник. get_older_bob порівнює два примірники Vessel і повертає рядок тексту. in_the_same_system порівнює два примірники Vessel і повертає bool.

Реалізуйте останні відсутні функції з відновлених рядків вище.

Редагувати через GitHub Посилання відкривається в новому вікні або вкладці
C++ Exercism

Час розпочати Доктор Даних?

Зареєструйтеся на Exercism, щоб вивчати й опановувати C++, а також 19 концепцій100 вправ та справжнє наставництво від людей, і все це безкоштовно.