En C++, les déclarations sont souvent séparées des définitions. Les déclarations sont regroupées dans ce qu'on appelle des fichiers d'en-tête, les implémentations correspondantes étant placées dans des fichiers source. On peut voir les fichiers d'en-tête comme une API. Le fichier d'en-tête te dit ce que propose une base de code, sans entrer dans les détails du comment.
L'extension de fichier la plus courante pour les fichiers d'en-tête est .h.
Certains projets utilisent .hpp ou se passent complètement d'extension.
Les définitions se trouvent dans un fichier .cpp séparé.
Pour réunir les deux parties, le fichier source commence par inclure le fichier d'en-tête correspondant.
Si tu veux écrire une bibliothèque appelée « quick_math » qui propose une fonction « super_root » que tu comptes utiliser souvent, les fichiers ressembleraient à ceci :
// 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 tu dois inclure un en-tête dont seule l'implémentation a besoin, la ligne #include correspondante n'est nécessaire que dans le fichier source.
Tout ce qui est inclus dans l'en-tête est également disponible dans le fichier .cpp, comme la bibliothèque string dans l'exemple ci-dessous.
Attention : le ; est nécessaire après la déclaration dans le fichier d'en-tête, mais pas après la définition dans le fichier source.
Beaucoup d'exercices C++ sur Exercism commencent par deux fichiers presque vides : un en-tête et un fichier source.
Tu dois consulter le fichier *_test.cpp pour découvrir les noms et les espaces de noms des fonctions attendues afin de résoudre l'exercice.
Les classes peuvent devenir très complexes, et leur rapport à la séparation entre en-tête et fichier source peut être déroutant. Une organisation possible consiste à garder tous les détails d'implémentation dans le fichier source, et toutes les déclarations et variables membres dans l'en-tête :
// 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;}
Quand l'en-tête sert de vue d'ensemble de l'API, c'est là qu'on cherche des informations comme les valeurs par défaut.
La valeur par défaut du paramètre size du constructeur est donc gérée dans l'en-tête et non dans l'implémentation.
Les définitions dans le fichier source sont préfixées par l'espace de noms robots et le type de classe Flower.
Une autre organisation possible est une bibliothèque header only, qui n'a pas du tout de fichier .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;}
};
}
Les projets peuvent combiner ces organisations, et les discussions vont bon train sur ce qui convient le mieux à chaque cas d'usage.
Tu as peut-être remarqué la ligne #pragma once dans le fichier d'en-tête d'exemple ci-dessus.
C'est ce qu'on appelle une garde d'inclusion, et elle garantit que le contenu du fichier n'est inclus qu'une seule fois pendant la compilation, afin d'éviter des erreurs.
Il existe une autre variante, plus complexe, de la garde d'inclusion, qui commence par #ifndef et se termine par #endif.
Elle remplit le même rôle, et son utilisation est illustrée dans l'exemple de la classe Flower ci-dessus.
Tu commences ton premier jour dans une entreprise australienne appelée « Doctor Data », spécialisée dans la récupération d'informations. Tu as brillé à ton entretien d'embauche grâce à tes connaissances en C++ et sur les sondes de von Neumann. Comme tu as déjà vu beaucoup de fichiers de tests, ton nouveau patron veut que tu recrées les fichiers source et d'en-tête correspondants à partir de code de test que l'entreprise a récemment récupéré.
Dans cet exercice, tu vas recréer des fichiers perdus. Pour l'instant, le contenu de ces fichiers n'est que des données inutilisables.
Le workflow de cet exercice d'apprentissage ressemble beaucoup à la structure des exercices d'entraînement d'Exercism. Le texte d'introduction de l'exercice ne constitue qu'une partie de la spécification. Le fichier de tests est ton guide de référence pour résoudre un problème donné. En raison du fonctionnement de la compilation en C++, les résultats des tests peuvent ne pas apparaître dans l'interface web tant que tu n'as pas implémenté une version minimale de chaque classe, fonction et énumération exigée par les tests. Si tu obtiens des erreurs de compilation, elles peuvent disparaître une fois que tu as traité toutes les tâches et tous les tests.
Tu dois accomplir quatre tâches, toutes liées à la récupération du code perdu dans les fichiers d'en-tête et source.
Tu regardes deux lignes récupérées des fichiers de tests :
heaven::Vessel bob{"Robert Johansson", 1};
heaven::Vessel will{"Riker", 2, star_map::System::BetaHydri};
Ton œil exercé a immédiatement reconnu un espace de noms heaven.
Tu vois aussi qu'il doit y avoir une classe appelée Vessel.
Le constructeur peut apparemment être appelé avec deux ou trois arguments.
Le premier argument semble être de type string, le deuxième est un nombre.
Il est possible d'initialiser la classe Vessel avec un troisième argument.
Le troisième argument vient d'un espace de noms star_map.
C'est un énumérateur de type System.
Tu as même récupéré l'une des énumérations : BetaHydri.
Prépare les fichiers source et d'en-tête avec les informations que tu as découvertes.
Il te faut deux espaces de noms : heaven et star_map.
L'espace de noms heaven contient une classe Vessel, qui peut être appelée avec deux ou trois arguments.
L'énumération System doit être placée dans l'espace de noms star_map et comporte un énumérateur BetaHydri.
Garde l'œil ouvert pour repérer d'autres énumérateurs, afin de pouvoir mettre à jour l'énumération System au fur et à mesure.
Tu découvres d'autres lignes du 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);
Les lignes de test que tu viens de trouver révèlent une autre fonction membre de la classe Vessel : replicate.
Cette fonction reçoit une string et renvoie une autre instance de Vessel.
Tu commences à te faire une idée de la valeur par défaut du troisième argument du constructeur grâce à la tâche précédente.
Tu vois aussi deux variables membres publiques de la classe Vessel ainsi que la spécification de la fonction membre replicate.
Ajoute la fonction replicate et les variables membres publiques current_system et generation aux fichiers d'en-tête et source.
Tu trouves d'autres lignes intéressantes dans les fichiers de tests récupérés :
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);
Apparemment, la classe Vessel possède une variable membre busters, que l'on peut modifier avec les deux fonctions membres de la classe make_buster et shoot_buster.
En attendant d'avoir d'autres informations, tu supposes que la fonction make_buster renvoie void.
Comme il y a un test sur la valeur de retour de shoot_buster, tu supposes que cette fonction renvoie un bool.
Ajoute les deux fonctions et la variable membre à la classe Vessel.
Continue à chercher d'autres énumérateurs System.
En parcourant les fichiers de tests, tu ne trouves que deux sections de code encore inexplorées :
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));
Tu vois deux fonctions qui ne sont pas des membres de la classe Vessel, puisqu'elles ne sont pas appelées sur une instance.
get_older_bob compare deux instances de Vessel et renvoie une string.
in_the_same_system compare deux instances de Vessel et renvoie un bool.
Implémente les dernières fonctions manquantes à partir des lignes récupérées ci-dessus.
Inscris-toi sur Exercism pour apprendre et maîtriser C++ avec 19 concepts100 exercices, et un vrai mentorat humain, le tout gratuitement.