Διαδρομές
/
C++
C++
/
Ασκήσεις
/
Δόκτωρ Δεδομένα
Δόκτωρ Δεδομένα

Δόκτωρ Δεδομένα

Άσκηση εκμάθησης

Εισαγωγή

Στη C++, οι δηλώσεις συχνά διαχωρίζονται από τους ορισμούς. Οι δηλώσεις ομαδοποιούνται σε αυτά που ονομάζουμε αρχεία κεφαλίδας, ενώ οι αντίστοιχες υλοποιήσεις τοποθετούνται σε αρχεία πηγαίου κώδικα. Μπορείς να σκεφτείς τα αρχεία κεφαλίδας σαν ένα API. Το αρχείο κεφαλίδας θα σου πει τι έχει να προσφέρει μια βάση κώδικα, χωρίς να μπει στις λεπτομέρειες του πώς.

Κεφαλίδα και πηγαίος κώδικας

Η πιο συνηθισμένη επέκταση αρχείου για τα αρχεία κεφαλίδας είναι το .h. Κάποια project χρησιμοποιούν .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 για να δεις τα ονόματα και τα namespaces των αναμενόμενων συναρτήσεων, ώστε να λύσεις την άσκηση.

Κλάσεις και κεφαλίδες

Οι κλάσεις μπορούν να γίνουν πολύ περίπλοκες και η σχέση τους με τον διαχωρισμό κεφαλίδας / πηγαίου κώδικα μπορεί να σε μπερδέψει. Μια πιθανή διάταξη είναι να κρατήσεις όλες τις λεπτομέρειες της υλοποίησης στο αρχείο πηγαίου κώδικα και όλες τις δηλώσεις και τις μεταβλητές μέλη στην κεφαλίδα:

// 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 του κατασκευαστή ορίζεται επομένως στην κεφαλίδα και όχι στην υλοποίηση. Οι ορισμοί στο αρχείο πηγαίου κώδικα έχουν μπροστά τους το namespace 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;}
    };
}

Τα project μπορεί να χρησιμοποιούν συνδυασμούς αυτών των διατάξεων και γίνεται μεγάλη συζήτηση για το ποια από αυτές ταιριάζει καλύτερα σε κάθε περίπτωση χρήσης.

Φρουροί συμπερίληψης

Ίσως πρόσεξες τη γραμμή #pragma once στο παραπάνω παράδειγμα αρχείου κεφαλίδας. Αυτό ονομάζεται φρουρός συμπερίληψης, και εξασφαλίζει ότι το περιεχόμενο του αρχείου συμπεριλαμβάνεται μόνο μία φορά κατά τη μεταγλώττιση, ώστε να αποφεύγονται σφάλματα. Υπάρχει μια άλλη, πιο σύνθετη παραλλαγή ενός φρουρού συμπερίληψης που ξεκινά με #ifndef και τελειώνει με #endif. Εξυπηρετεί τον ίδιο σκοπό και η χρήση της φαίνεται στο παραπάνω παράδειγμα της κλάσης Flower.

Οδηγίες

Ξεκινάς την πρώτη σου μέρα σε μια αυστραλιανή εταιρεία που λέγεται "Doctor Data" και ειδικεύεται στην ανάκτηση πληροφοριών. Πέρασες τη συνέντευξη για τη δουλειά με άνεση, χάρη στις γνώσεις σου στη C++ και στους ανιχνευτές von Neumann. Επειδή έχεις δει πολλά αρχεία δοκιμών, το νέο σου αφεντικό θέλει να αναδημιουργήσεις τα αντίστοιχα αρχεία πηγαίου κώδικα και κεφαλίδας από έναν κώδικα δοκιμών που ανέκτησε πρόσφατα η εταιρεία.

Σε αυτήν την άσκηση θα αναδημιουργήσεις χαμένα αρχεία. Αυτή τη στιγμή, το περιεχόμενο των αρχείων είναι άχρηστα σκουπίδια δεδομένων.

Note

Η ροή εργασίας αυτής της άσκησης εννοιών μοιάζει πολύ με τη δομή των ασκήσεων πρακτικής του Exercism. Το κείμενο εισαγωγής της άσκησης είναι μόνο ένα μέρος των προδιαγραφών. Το αρχείο δοκιμών είναι ο απόλυτος οδηγός σου για να λύσεις ένα δεδομένο πρόβλημα. Εξαιτίας του τρόπου με τον οποίο λειτουργεί η μεταγλώττιση της C++, τα αποτελέσματα των δοκιμών μπορεί να μην εμφανιστούν στη διαδικτυακή διεπαφή μέχρι να υλοποιήσεις μια ελάχιστη έκδοση κάθε κλάσης, συνάρτησης και απαρίθμησης που απαιτούν οι δοκιμές. Αν λάβεις σφάλματα μεταγλώττισης, μπορεί να εξαφανιστούν μόλις καλύψεις όλες τις εργασίες και τις δοκιμές.

Έχεις τέσσερις εργασίες, όλες σχετικές με την ανάκτηση του χαμένου κώδικα στα αρχεία κεφαλίδας και στα αρχεία πηγαίου κώδικα.

1. Προσπάθησε να εξαγάγεις μερικά πρώτα στοιχεία

Κοιτάς δύο ανακτημένες γραμμές από τα αρχεία δοκιμών:

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

Το κοφτερό σου μάτι αναγνώρισε αμέσως ένα namespace heaven. Βλέπεις επίσης ότι πρέπει να υπάρχει μια κλάση που λέγεται Vessel. Ο κατασκευαστής μπορεί προφανώς να κληθεί με δύο ή τρία ορίσματα. Το πρώτο όρισμα φαίνεται να είναι τύπου string, το δεύτερο είναι ένας αριθμός. Είναι δυνατό να αρχικοποιήσεις την κλάση Vessel με ένα τρίτο όρισμα. Το τρίτο όρισμα προέρχεται από ένα namespace star_map. Είναι ένας απαριθμητής τύπου System. Πήρες μάλιστα και μία από τις απαριθμήσεις: BetaHydri.

Ετοίμασε τα αρχεία πηγαίου κώδικα και κεφαλίδας με τις πληροφορίες που ανακάλυψες. Χρειάζεσαι δύο namespaces: heaven και star_map. Το namespace heaven έχει μια κλάση Vessel, που μπορεί να κληθεί με δύο ή τρία ορίσματα. Το enum System πρέπει να τοποθετηθεί στο namespace star_map και έχει μια απαρίθμηση BetaHydri. Να έχεις το νου σου για άλλες απαριθμήσεις, ώστε να ενημερώνεις συνεχώς το enum 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. Αυτή η συνάρτηση δέχεται μια συμβολοσειρά και επιστρέφει ένα άλλο στιγμιότυπο της 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 ασκήσεις και πραγματική καθοδήγηση από ανθρώπους, όλα δωρεάν.