Tracks
/
C++
C++
/
Übungen
/
Doctor Data
Doctor Data

Doctor Data

Lernübung

Einführung

In C++ werden Deklarationen oft von Definitionen getrennt. Deklarationen werden in sogenannten Header-Dateien gruppiert, während die jeweiligen Implementierungen in Quelldateien liegen. du kannst dir die Header-Dateien wie eine API vorstellen. Die Header-Datei sagt dir, was eine Codebasis zu bieten hat, ohne auf die Details einzugehen, wie.

Header und Quelldateien

Die häufigste Dateiendung für Header-Dateien ist .h. Manche Projekte verwenden .hpp oder lassen die Endung ganz weg.

Die Definitionen befinden sich in einer separaten .cpp-Datei. Um die Teile wieder zusammenzuführen, beginnt die Quelldatei damit, die jeweilige Header-Datei einzubinden.

Wenn du eine Bibliothek namens „quick_math“ schreiben möchtest, die eine Funktion „super_root“ anbietet, die du oft benutzt, sähen die Dateien so aus:

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

Wenn du einen Header einbinden musst, der nur für die Implementierung benötigt wird, wird die jeweilige #include-Zeile nur in der Quelldatei gebraucht. Alles, was im Header eingebunden wird, ist auch in der .cpp-Datei verfügbar, wie die string-Bibliothek im Beispiel unten. Achtung: Das ; wird nach der Deklaration in der Header-Datei benötigt, aber nicht nach der Definition in der Quelldatei.

Note

Viele C++-Übungen auf Exercism beginnen mit zwei fast leeren Dateien: Header und Quelldatei. du musst die *_test.cpp-Datei prüfen, um die Namen und Namensräume der erwarteten Funktionen zu sehen, damit du die Übung lösen kannst.

Klassen und Header

Klassen können sehr komplex werden, und ihr Verhältnis zur Aufteilung in Header und Quelldatei kann verwirrend sein. Eine mögliche Aufteilung ist, alle Implementierungsdetails in der Quelldatei zu belassen und alle Deklarationen und Membervariablen in den Header zu schreiben:

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

Wenn der Header als API-Übersicht verwendet wird, schaut man dort nach Informationen wie Standardwerten. Der Standardwert des size-Parameters des Konstruktors wird daher im Header behandelt und nicht in der Implementierung. Die Definitionen in der Quelldatei werden mit dem Namensraum robots und dem Klassentyp Flower versehen.

Eine weitere Möglichkeit ist eine Header-only-Bibliothek, die überhaupt keine .cpp-Datei hat:

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

Projekte verwenden vielleicht Kombinationen dieser Aufteilungen, und es gibt viele Diskussionen darüber, was für den jeweiligen Anwendungsfall am besten geeignet ist.

Include-Guards

du hast vielleicht die #pragma once-Zeile in der Header-Datei aus dem obigen Beispiel bemerkt. Das nennt man einen Include-Guard, und er stellt sicher, dass der Inhalt der Datei während der Kompilierung nur einmal eingebunden wird, um Fehler zu vermeiden. Es gibt eine weitere, komplexere Variante eines Include-Guards, die mit #ifndef beginnt und mit #endif endet. Sie dient demselben Zweck, und ihre Verwendung ist im obigen Beispiel der Klasse Flower gezeigt.

Anleitung

Du beginnst deinen ersten Arbeitstag bei einem australischen Unternehmen namens „Doctor Data“, das sich auf die Wiederherstellung von Informationen spezialisiert hat. Du hast dein Bewerbungsgespräch mit deinem Wissen über C++ und von-Neumann-Sonden gemeistert. Da du schon viele Testdateien gesehen hast, möchte dein neuer Chef, dass du die zugehörigen Quell- und Header-Dateien aus einem Testcode rekonstruierst, den das Unternehmen kürzlich wiederhergestellt hat.

In dieser Übung rekonstruierst du verlorene Dateien. Derzeit sind die Inhalte der Dateien unbrauchbarer Datenmüll.

Note

Der Workflow dieser Konzeptübung ist dem Aufbau der Praxisübungen von Exercism sehr ähnlich. Der Einführungstext der Übung ist nur ein Teil der Spezifikation. Die Testdatei ist dein maßgeblicher Leitfaden, um ein gegebenes Problem zu lösen. Weil die C++-Kompilierung nun einmal so funktioniert, erscheinen die Testergebnisse in der Weboberfläche möglicherweise erst dann, wenn du eine minimale Version jeder Klasse, Funktion und Aufzählung implementiert hast, die die Tests verlangen. Wenn du Kompilierungsfehler erhältst, verschwinden sie vielleicht, sobald du alle Aufgaben und Tests bearbeitet hast.

Du hast vier Aufgaben, die alle damit zu tun haben, den verlorenen Code in den Header- und Quelldateien wiederherzustellen.

1. Versuche, erste Hinweise zu finden

Du siehst dir zwei wiederhergestellte Zeilen aus den Testdateien an:

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

Dein scharfer Blick erkennt sofort einen Namensraum heaven. Du siehst auch, dass es eine Klasse namens Vessel geben muss. Der Konstruktor lässt sich offenbar mit zwei oder drei Argumenten aufrufen. Das erste Argument scheint vom Typ string zu sein, das zweite ist eine Zahl. Es ist möglich, die Klasse Vessel mit einem dritten Argument zu initialisieren. Das dritte Argument stammt aus einem star_map-Namensraum. Es ist ein Aufzählungswert vom Typ System. Du hast sogar einen der Aufzählungswerte: BetaHydri.

Bereite die Quell- und Header-Dateien mit den Informationen vor, die du entdeckt hast. Du brauchst zwei Namensräume: heaven und star_map. Der Namensraum heaven enthält eine Klasse Vessel, die sich mit zwei oder drei Argumenten aufrufen lässt. Die System-Aufzählung gehört in den Namensraum star_map und enthält einen Aufzählungswert BetaHydri. Halte nach weiteren Aufzählungswerten Ausschau, damit du die System-Aufzählung laufend ergänzen kannst.

2. Finde weitere Details

Du findest weitere Zeilen aus dem Test:

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

Die neu gefundenen Testzeilen offenbaren eine weitere Memberfunktion der Klasse Vessel: replicate. Diese Funktion nimmt einen String entgegen und gibt eine weitere Vessel-Instanz zurück. Aus der vorherigen Aufgabe bekommst du einen Hinweis auf den Standardwert des dritten Arguments des Konstruktors. Außerdem siehst du zwei öffentliche Membervariablen der Klasse Vessel und die Spezifikation der Memberfunktion replicate.

Füge die Funktion replicate und die öffentlichen Membervariablen current_system und generation zu den Header- und Quelldateien hinzu.

3. Sieh dir die inneren Mechanismen an

Du findest noch ein paar interessante Zeilen in den wiederhergestellten Testdateien:

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

Anscheinend hat die Klasse Vessel eine Membervariable busters, die sich mit den beiden Memberfunktionen make_buster und shoot_buster ändern lässt. Solange keine weiteren Informationen auftauchen, vermutest du, dass die Funktion make_buster void zurückgibt. Da es einen Test für den Rückgabewert von shoot_buster gibt, nimmst du an, dass die Funktion einen bool zurückgibt.

Füge die beiden Funktionen und die Membervariable zur Klasse Vessel hinzu. Suche weiter nach weiteren System-Aufzählungswerten.

4. Vervollständige das Bild

Beim Durchsuchen der Testdateien stößt du nur auf zwei noch nicht abgedeckte Codeabschnitte:

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

Du siehst zwei Funktionen, die keine Member der Klasse Vessel sind, weil sie nicht mit einer Instanz aufgerufen werden. get_older_bob vergleicht zwei Vessel-Instanzen und gibt einen string zurück. in_the_same_system vergleicht zwei Vessel-Instanzen und gibt einen bool zurück.

Implementiere die letzten fehlenden Funktionen aus den oben wiederhergestellten Zeilen.

Über GitHub bearbeiten Der Link öffnet sich in einem neuen Fenster oder Tab
C++ Exercism

Bereit, mit Doctor Data zu starten?

Melde dich bei Exercism an, um C++ mit 19 Konzepte100 Übungen und echtem menschlichen Mentoring zu lernen und zu meistern, alles kostenlos.